Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 499
Vittorio Romeo Avatar asked Aug 09 '19 23:08

Vittorio Romeo


People also ask

Where is decltype used?

In C++11, you can use the decltype type specifier on a trailing return type, together with the auto keyword, to declare a function template whose return type depends on the types of its template arguments.

What is the difference between auto and decltype?

'auto' lets you declare a variable with a particular type whereas decltype lets you extract the type from the variable so decltype is sort of an operator that evaluates the type of passed expression.

Should you use auto C++?

Automatic type deduction is one of the most important and widely used features in modern C++. The new C++ standards have made it possible to use auto as a placeholder for types in various contexts and let the compiler deduce the actual type.


1 Answers

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.

like image 117
L. F. Avatar answered Sep 19 '22 21:09

L. F.