Are there any realistic use cases for `decltype(auto)` variables?

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.


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.