Both from my personal experience and from consulting answers to questions like What are some uses of decltype(auto)? I can find plenty of valuable use cases for decltype(a
Probably not a very deep answer, but basically decltype(auto)
was proposed to be used for return type deduction, to be able to deduce references when the return type is actually a reference (contrary to plain auto
that will never deduce the reference, or auto&&
that will always do it).
The fact that it can also be used for variable declaration not necessarily means that there should be better-than-other scenarios. Indeed, using decltype(auto)
in variable declaration will just complicate the code reading, given that, for a variable declaration, is has exactly the same meaning. On the other hand, the auto&&
form allows you to declare a constant variable, while decltype(auto)
doesn't.
Essentially, the case for variables is the same for functions. The idea is that we store the result of an function invocation with a decltype(auto)
variable:
decltype(auto) result = /* function invocation */;
Then, result
is
a non-reference type if the result is a prvalue,
a (possibly cv-qualified) lvalue reference type if the result is a lvalue, or
an rvalue reference type if the result is an xvalue.
Now we need a new version of forward
to differentiate between the prvalue case and the xvalue case: (the name forward
is avoided to prevent ADL problems)
template <typename T>
T my_forward(std::remove_reference_t<T>& arg)
{
return std::forward<T>(arg);
}
And then use
my_forward<decltype(result)>(result)
Unlike std::forward
, this function is used to forward decltype(auto)
variables. Therefore, it does not unconditionally return a reference type, and it is supposed to be called with decltype(variable)
, which can be T
, T&
, or T&&
, so that it can differentiate between lvalues, xvalues, and prvalues. Thus, if result
is
a non-reference type, then the second overload is called with a non-reference T
, and a non-reference type is returned, resulting in a prvalue;
an lvalue reference type, then the first overload is called with a T&
, and T&
is returned, resulting in an lvalue;
an rvalue reference type, then the second overload is called with a T&&
, and T&&
is returned, resulting in an xvalue.
Here's an example. Consider that you want to wrap std::invoke
and print something to the log: (the example is for illustration only)
template <typename F, typename... Args>
decltype(auto) my_invoke(F&& f, Args&&... args)
{
decltype(auto) result = std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
my_log("invoke", result); // for illustration only
return my_forward<decltype(result)>(result);
}
Now, if the invocation expression is
a prvalue, then result
is a non-reference type, and the function returns a non-reference type;
a non-const lvalue, then result
is a non-const lvalue reference, and the function returns a non-const lvalue reference type;
a const lvalue, then result
is a const lvalue reference, and the function returns a const lvalue reference type;
an xvalue, then result
is an rvalue reference type, and the function returns an rvalue reference type.
Given the following functions:
int f();
int& g();
const int& h();
int&& i();
the following assertions hold:
static_assert(std::is_same_v<decltype(my_invoke(f)), int>);
static_assert(std::is_same_v<decltype(my_invoke(g)), int&>);
static_assert(std::is_same_v<decltype(my_invoke(h)), const int&>);
static_assert(std::is_same_v<decltype(my_invoke(i)), int&&>);
(live demo, move only test case)
If auto&&
is used instead, the code will have some trouble differentiating between prvalues and xvalues.