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

2

28

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.

Airport answered 9/8, 2019 at 23:40 Comment(4)
How do you define "realistic"?Schism
@NicolBolas: I am looking for some 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 not declaring a variable at all. The domain doesn't matter, it could be some obscure metaprogramming corner case. But the example would need to make me go "Hey, that's clever!" and using any other feature to achieve the same effect would require more boilerplate or have some sort of drawback. Sorry that I cannot be more precise.Airport
@Eljay: I'm flattered! I can give you plenty of examples for decltype(auto)-returning functions... but variables baffle me at the moment :)Airport
I recently made use of decltype(auto) variables for NRVO like this: godbolt.org/z/9oz37Was1Tsaritsyn
T
6

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.

Timi answered 14/8, 2019 at 14:16 Comment(2)
I don’t see how auto&& allows you to declare a constant variable (references are never const at the top level) whereas decltype(auto) allows you to do that without any problem.Ernaline
@L.F. sorry for the late respond, probably I expressed incompletetly: I meant that there are certain cases where you can convert to constant by adding the const modifier in auto&&, whereas decltype(auto) doesn't allow you to do so. I know it is not possible to use in all the scenarios (as in int a; int& b=a; const auto&& c=b;), but it is in cases like int foo() { return 0; } const auto&& a=foo();. Probably not very useful but possible. Anyway, please correct me if I'm missing anything.Timi
E
18

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.

Ernaline answered 10/8, 2019 at 9:9 Comment(9)
I think this is a step in the right direction, but your example causes an unnecessary copy when f returns a prvalue, and inhibits mandatory copy elision. Take a look at my version here: gcc.godbolt.org/z/awuVxyAirport
Posted a follow up question here: #57445393 - I am not convinced that decltype(auto) is the best solution to this problem. The on_scope_success solution seems cleaner.Airport
@VittorioRomeo: "I am not convinced that decltype(auto) is the best solution to this problem." And that's what makes your question opinion-based: who decides what "best" is? LF's code is extremely readable, while your alternative example is... weird. It's not clear why it is you're doing what you're doing or what the purpose of it is. It's a lot of additional code just to maybe avoid a copy/move (since pretty much every compiler will NVRO that code).Schism
@VittorioRomeo: Indeed, your own definition of "realistic" specifically states "using any other feature to achieve the same effect would require more boilerplate". Well, your alternative definitely requires more boilerplate. So by your own definition, this is a "realistic" example.Schism
@NicolBolas: it's not about NRVO, the version presented by this answer introduces an extra copy! See gcc.godbolt.org/z/awuVxy. I don't see that as a minor drawback, I see that as a major flaw. As an example, it prevents usage of move-only objects. So no, it doesn't achieve the "same effect" as my version. If the extra copy doesn't matter we might as well use auto or auto&& instead of decltype(auto).Airport
@VittorioRomeo You are right, my previous answer is completely wrong. I have updated my answer with a better version (not resorting to if constexpr). I also added a move only type test case.Ernaline
@L.F.: this still seems to be wrong. Any prvalue gets copied, like before. And RVO is not possible. See gcc.godbolt.org/z/gNCk_xAirport
@Vittorio I am soooo stupid. Now it should be right.Ernaline
@L.F.: now it moves instead of copying, but prevents RVO as far as I can see.Airport
T
6

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.

Timi answered 14/8, 2019 at 14:16 Comment(2)
I don’t see how auto&& allows you to declare a constant variable (references are never const at the top level) whereas decltype(auto) allows you to do that without any problem.Ernaline
@L.F. sorry for the late respond, probably I expressed incompletetly: I meant that there are certain cases where you can convert to constant by adding the const modifier in auto&&, whereas decltype(auto) doesn't allow you to do so. I know it is not possible to use in all the scenarios (as in int a; int& b=a; const auto&& c=b;), but it is in cases like int foo() { return 0; } const auto&& a=foo();. Probably not very useful but possible. Anyway, please correct me if I'm missing anything.Timi

© 2022 - 2024 — McMap. All rights reserved.