Vittorio Romeo
Vittorio Romeo

Reputation: 93284

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

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(auto) as a function return type placeholder.

However, I am seriously struggling to think of any valid (i.e. useful, realistic, valuable) use case for decltype(auto) variables. The only possibility that comes to mind is to store the result of a function returning decltype(auto) for later propagation, but auto&& could be used there as well and it would be simpler.

I've even searched throughout all my projects and experiments, and the 391 occurrences of decltype(auto) are all return type placeholders.

So, are there any realistic use cases for decltype(auto) variables? Or it this feature only useful when used as a return type placeholder?


How do you define "realistic"?

I am looking for a use case that provides value (i.e. it's not just an example to show how the feature works) where decltype(auto) is the perfect choice, compared to alternatives such as auto&& or to not declaring a variable at all.

The problem domain doesn't matter, it could be some obscure metaprogramming corner case or arcane functional programming construct. However, the example would need to make me go "Hey, that's clever/beautiful!" and using any other feature to achieve the same effect would require more boilerplate or have some sort of drawback.

Upvotes: 28

Views: 1041

Answers (2)

L. F.
L. F.

Reputation: 20579

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.

Upvotes: 18

cbuchart
cbuchart

Reputation: 11555

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.

Upvotes: 6

Related Questions