Search code examples
c++c++11c++14move-semanticsrvalue-reference

Return forwarding reference parameters - Best practice


In the following scenario

template <class T>
? f(T&& a, T&& b)
{
    return a > b ? a : b; 
}

what would the optimum return type be ? My thoughts so far are :

  1. Return by r value refs, perfectly forwarding the function arguments :

    template <class T>
    decltype(auto) f(T&& a, T&& b)
    {
        return a > b ? forward<T>(a) : forward<T>(b); 
    }
    
  2. Move construct the return value :

    template <class T>
    auto f(T&& a, T&& b)
    {
        return a > b ? forward<T>(a) : forward<T>(b); 
    }
    
  3. Try fo find a way to enable (N)RVOs (even though I think since I want to use the function arguments that's impossible)

Is there a problem with these solutions ? Is there a better one ? What's the best practice ?


Solution

  • It depends on your intentions and the move awareness of your T. Each case is valid but there are differences in the behavior; this :

    template <class T>
    decltype(auto) f(T&& a, T&& b)
    {
        return a > b ? forward<T>(a) : forward<T>(b); 
    }
    

    will perfectly forward an lvalue or rvalue reference resulting in no temporaries at all. If you put an inline there (or the compiler puts it for you) is like having the parameter in place. Against "first sight intuition" it won't produce any runtime error due to 'referencing' a dead stack object (for the rvalue case), since any temporaries we forward come from a caller above us (or below us, depends on how you think of stacks during function calls). On the other hand, this (in the rvalue case):

    template <class T>
    auto f(T&& a, T&& b)
    {
        return a > b ? forward<T>(a) : forward<T>(b); 
    }
    

    will move-construct a value (auto will never deduce a & or &&) and if T is not move constructible, then you'll invoke a copy constructor (a copy will always be made if T is an lvalue reference). If you use f in an expression you'll produce an xvalue after all (and that makes me wonder if the compiler can use the initial rvalue, but I wouldn't bet on it).

    Long story short, if the code that uses f is build in a way that handles both rvalue and lvalue references, I'd go with the first choice, after all that's the logic std::forward is built around plus you won't produce any copies when you have lvalue references.