Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perfect-forwarding a return value with auto&&

Consider this quote from C++ Templates: The Complete Guide (2nd Edition):

decltype(auto) ret{std::invoke(std::forward<Callable>(op),
                               std::forward<Args>(args)...)};
...
return ret;

Note that declaring ret with auto&& is not correct. As a reference, auto&& extends the lifetime of the returned value until the end of its scope but not beyond the return statement to the caller of the function.

The author says that auto&& is not appropriate for perfect-forwarding a return value. However, doesn't decltype(auto) also form a reference to xvalue/lvalue? IMO, decltype(auto) then suffers from the same issue. Then, what's the point of the author?

EDIT:

The above code snippet shall go inside this function template.

template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
    // here
}
like image 717
b1sub Avatar asked Feb 28 '18 16:02

b1sub


People also ask

What is c++ perfect forwarding?

Perfect forwarding allows a template function that accepts a set of arguments to forward these arguments to another function whilst retaining the lvalue or rvalue nature of the original function arguments.

What does std forward?

std::forward has a single use case: to cast a templated function parameter (inside the function) to the value category (lvalue or rvalue) the caller used to pass it. This allows rvalue arguments to be passed on as rvalues, and lvalues to be passed on as lvalues, a scheme called “perfect forwarding.”

What is forwarding reference in c++?

When t is a forwarding reference (a function argument that is declared as an rvalue reference to a cv-unqualified function template parameter), this overload forwards the argument to another function with the value category it had when passed to the calling function.


3 Answers

There are two deductions here. One from the return expression, and one from the std::invoke expression. Because decltype(auto) is deduced to be the declared type for unparenthesized id-expression, we can focus on the deduction from the std::invoke expression.

Quoted from [dcl.type.auto.deduct] paragraph 5:

If the placeholder is the decltype(auto) type-specifier, T shall be the placeholder alone. The type deduced for T is determined as described in [dcl.type.simple], as though e had been the operand of the decltype.

And quoted from [dcl.type.simple] paragraph 4:

For an expression e, the type denoted by decltype(e) is defined as follows:

  • if e is an unparenthesized id-expression naming a structured binding ([dcl.struct.bind]), decltype(e) is the referenced type as given in the specification of the structured binding declaration;

  • otherwise, if e is an unparenthesized id-expression or an unparenthesized class member access, decltype(e) is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions, the program is ill-formed;

  • otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e;

  • otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;

  • otherwise, decltype(e) is the type of e.

Note decltype(e) is deduced to be T instead of T&& if e is a prvalue. This is the difference from auto&&.

So if std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...) is a prvalue, for example, the return type of Callable is not a reference, i.e. returning by value, ret is deduced to be the same type instead of a reference, which perfectly forwards the semantic of returning by value.

like image 89
xskxzr Avatar answered Oct 13 '22 06:10

xskxzr


auto&& is always a reference type. On the other hand, decltype(auto) can be either a reference or a value type, depending on the initialiser used.

Since ret in the return statement is not surrounded by parenthesis, call()'s deduced return type only depends on the declared type of the entity ret, and not on the value category of the expression ret:

template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
   decltype(auto) ret{std::invoke(std::forward<Callable>(op),
                                  std::forward<Args>(args)...)};
   ...
   return ret;
}

If Callable returns by value, then the value category of op's call expression will be a prvalue. In that case:

  • decltype(auto) will deduce res as a non-reference type (i.e., value type).
  • auto&& would deduce res as a reference type.

As explained above, the decltype(auto) at call()'s return type simply results in the same type as res. Therefore, if auto&& would have been used for deducing the type of res instead of decltype(auto), call()'s return type would have been a reference to the local object ret, which does not exist after call() returns.

like image 1
ネロク・ゴ Avatar answered Oct 13 '22 05:10

ネロク・ゴ


However, doesn't decltype(auto) also form a reference to xvalue/lvalue?

No.

Part of decltype(auto)'s magic is that it knows ret is an lvalue, so it will not form a reference.

If you'd written return (ret), it would indeed have resolved to a reference type and you'd be returning a reference to a local variable.

tl;dr: decltype(auto) is not always the same as auto&&.

like image 6
Lightness Races in Orbit Avatar answered Oct 13 '22 06:10

Lightness Races in Orbit