How to return forwarding references?

Viewed 325

What is the most natural way to return a forward (universal) reference?

struct B{
         template<class Ar> friend
/*   1*/ Ar&& operator<<(Ar&& ar, A const& a){...; return std::forward<Archive>(ar);}
         template<class Ar> friend 
/*OR 2*/ Ar& operator<<(Ar&& ar, A const& a){...; return ar;}
         template<class Ar> friend 
/*OR 3*/ Ar  operator<<(Ar&& ar, A const& a){...; return std::forward<Ar>(ar);}
         template<class Ar> friend 
/*OR 4*/ Ar  operator<<(Ar&& ar, A const& a){...; return ar;}
/*OR 5*/ other??
}

The case I have in mind is a stream-like object that is constructed and immediately used. For example:

dosomethign( myarchive_type{} << B{} << B{} << other_unrelated_type{} );
1 Answers

It is very rarely a good idea to return an rvalue reference. The rare exceptions are cases where you are writing a function logically equivalent to std::forward.

The reason why it is a bad idea is that reference lifetime extension is not transitive, and you can easily get dangling references accidentally.

const auto& foo = some_operation;

if some_operation is a prvalue expression, the lifetime of the temporary is extended to that of foo. However, if some_operation is instead:

foo( some_prvalue_expression )

where foo takes an rvalue reference and returns it, instead you silently get a dangling reference.

For types that are cheap to move, the solution is simple:

struct B{
  template<class Ar> friend
  Ar operator<<(Ar&& ar, A const& a){...; return std::forward<Archive>(ar);}
}

This returns by reference if passed an lvalue, but by value if passed an rvalue.

Now reference lifetime extension works correctly.

This requires that your type be cheap-to-move; so

myarchive_type{} << B{} << B{} << other_unrelated_type{};

results in the myarchive_type{} being moved 3 times.

If your type isn't cheap to move, consider simply blocking the rvalue version; the lack of safety makes it not worth it.

For a concrete example, look at this function:

template<class C>
struct backwards {
  C c;
  auto begin() const {
    using std::rbegin;
    return rbegin(c);
  }
  auto end() const {
    using std::rend;
    return rend(c);
  }
};
template<class C>
backwards(C&&) -> backwards<C>;

(Apologies if I got the deduction guide wrong).

Now we do this:

for( auto x : backwards{ std::vector<int>{1,2,3} } ) {
  std::cout << x << ",";
}

the vector is moved into backwards, but if we do this:

auto vec = std::vector<int>{1,2,3};
for( auto x : backwards{ vec } ) {
  std::cout << x << ",";
}

no copy is made.

Without the "create a copy and return it if we are passed an rvalue", the first for loop above instead has a dangling reference in it.


I've seen a few proposals or ideas for proposals to try to generalize reference lifetime extension, or at least provide compilers with hints that the return value of a function depends on the lifetime of the arguments passed to it so that it can generate errors/warnings when you are using dangling reference this way. None, as far as I know, have made it into a C++ standard.

Until one does, casually returning an rvalue reference is simply too dangerous for my blood.


Suppose you consider moving expensive. Well this works:

std::invoke([&](auto&& ar){
  dosomething( ar << << B{} << B{} << other_unrelated_type{} );
}, myarchive_type{} );

it takes the temporary myarchive_type{} and stores it in an rvalue reference.

Then it passes it to a lambda. Inside the lambda, the name ar is an lvalue. We proceed to use it safely within the lambda.

Related