Question

Generic Perfect Forwarding of Function’s Result

I encountered an issue when trying to perfect forward a function’s result in a generic way.

Here two functions that provide a result:

Foo provideFooAsTemporary() 
{
    return Foo{};
}

Foo& provideFooAsReference() 
{
    static Foo foo{};
    return foo;
}

Here the erroneous perfect forwarding:

template <typename TFn>
decltype(auto) erroneousForwardFoo(TFn&& fn) 
{
        auto&& result = fn();

        // do some stuff here

        return std::forward<decltype(result)>(result);
}

Foo fooTemporary = erroneousForwardFoo(provideFooAsTemporary); // fails
Foo& fooReference = erroneousForwardFoo(provideFooAsReference); // works

When the result is a temporary, the forwarding fails, which is indicated by the result from erroneousForwardFoo pointing to invalid memory.

In contrast, the following simplified forwarding code works like a charm:

template <typename TFn>
decltype(auto) workingForwardFoo(TFn&& fn) 
{
        return fn();
}

Foo fooTemporary = workingForwardFoo(provideFooAsTemporary); // works
Foo& fooReference = workingForwardFoo(provideFooAsReference); // works

However, I want to "do some stuff" before returning, so I need a fix for erroneousForwardFoo.

Debugging has shown that a dangling reference is the problem (see godbolt example). As I understand, this is because the lifetime of the temporary is bound to the rvalue reference result, and unfortunately moving doesn’t change this.

Now the questions:

  1. What is the detailed and accurate explanation of what’s going wrong?
  2. How can I rewrite erroneousForwardFoo in a lean way to achieve a correct generic implementation?

EDIT 1: Although I only provided a code example for forwarding a lvalue reference, I want the solution to support rvalue references too:

Foo&& provideFooAsRValueReference() 
{
    static Foo foo{};
    return std::move(foo);
}
 3  120  3
1 Jan 1970

Solution

 1

This has nothing to do with perfect forwarding. Instead, the solution is to realise that the return type differs in the two cases and that you have to test for that explicitly so that you can handle it differently.

The following code does what you want, in that no unnecessary copies are made:

template <typename TFn>
auto forwardFoo (TFn &&fn) -> decltype (fn ())
{
    if constexpr (!std::is_reference_v <decltype (fn ())>)
    {
        auto result = fn ();
        do_stuff (result);
        return result;
    }
    else
    {
        auto &&result = fn ();
        do_stuff (result);
        return std::forward<decltype (result)> (result);
    }        
}

There are a couple of points of interest here:

  1. You can't use plain auto as the return type since the compiler cannot deduce what you want (there being two to choose from). Instead, you need to specify it explicitly.

  2. You need to put the 'temporary' case before the 'reference' case, otherwise gcc copies the temporary in the return statement (clang makes a better job of it, FWIW).

  3. Since do_stuff has to be invoked in two separate places, you'll need to hoist it out into a separate function.

Live demo

2024-07-10
catnip

Solution

 1

decltype(auto) can be used for objects too. I would simply use static_cast:

decltype(auto) forwarder(auto&& ...fn_or_arg){
    decltype(auto) res = std::invoke(std::forward<decltype(fn_or_arg)>(fn_or_arg)...);

    /*Manipulate(res)...*/;

    return static_cast<decltype(res)>(res);
};

If compiler has difficulty with direct initialization and copy elision, it is possible to return conditionally:

using res_t = decltype(res);
if  constexpr (std::is_reference_v<res_t>)
    return std::forward<res_t>(res);
else
    return res;
2024-07-11
Red.Wave

Solution

 0

All credits to Red.Wave who has already nailed it. Nevertheless, I would like to share a slight change, which I think is more expressive. That is, treating the special case of preserving rvalue references as explicit as possible by using std::move instead of std::forward:

template <typename TFn>
decltype(auto) forward(TFn&& fn) 
{
    decltype(auto) result = fn();

    // do some stuff here

    if constexpr (std::is_rvalue_reference_v<decltype(result)>)
        return std::move(result);
    else
        return result;
}
2024-07-12
mahush