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.