why use decltype(auto) return type instead of auto?

Hi,

I have this code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace detail {
template <class F, class Tuple, std::size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, std::index_sequence<I...>)
{
  return std::invoke(std::forward<F>(f), std::get<I>(std::forward<Tuple>(t))...);		}
} // namespace detail

template <class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& t)
{
   return detail::apply_impl(std::forward<F>(f), std::forward<Tuple>(t),
			std::make_index_sequence < 
    std::tuple_size<std::decay_t<Tuple>>::value > {});
}


What difference does it make that return types are decltype(auto) instead of auto??

In which cases do we want to use decltype(auto) vrs auto?

Regards,
Juan
Last edited on
1
2
3
int   prvalue() { return 42; }                                
int&  lvalue()  { static int r; return r; }                     
int&& xvalue()  { static int r; return static_cast<int&&>(r); }

The value category rules say the expressions prvalue(), lvalue(), and xvalue(), are prvalue, lvalue, and xvalue expressions respectively.

We can assert the truth of that claim to the compiler:
1
2
3
4
#include <concepts>
static_assert(std::same_as<decltype(prvalue()), int>,   "prvalue() is a prvalue expression");
static_assert(std::same_as<decltype(lvalue()),  int&>,  "lvalue() is a lvalue expression");
static_assert(std::same_as<decltype(xvalue()),  int&&>, "xvalue() is a xvalue expression");

A key point is that decltype produces reference types that may reveal the value category of the expression passed into it. The static_asserts above use that property to check the value category of calls to our test functions.

Consider this function:
decltype(auto) a(auto f) { return f(); }
To decide the return type, the compiler puts the initializer in a's return statement into decltype. For example, a(prvalue) uses decltype(prvalue()) as its return type.
1
2
3
static_assert(std::same_as<decltype(a(prvalue)), int>,   "a(prvalue) is a prvalue expression");  
static_assert(std::same_as<decltype(a(lvalue)),  int&>,  "a(lvalue) is a lvalue expression");  
static_assert(std::same_as<decltype(a(xvalue)),  int&&>, "a(xvalue) is a xvalue expression");


Compare that to
auto b(auto f) { return f(); }
In this case, the compiler uses the type deduced for T as the return type, where T is a parameter of an imaginary function template like this one:
template <typename T> void ftad(T f)

For example, b(xvalue) returns int since ftad(xvalue()) causes the compiler to deduce the type int for T.
1
2
3
static_assert(std::same_as<decltype(b(prvalue)), int>, "b(prvalue) is an prvalue expression");
static_assert(std::same_as<decltype(b(lvalue)),  int>, "b(lvalue) is an prvalue expression");
static_assert(std::same_as<decltype(b(xvalue)),  int>, "b(xvalue) is an prvalue expression");


decltype(auto) was the right choice for apply, because if invoke returned some kind of a reference you'd want apply to do the same. If apply used auto instead, a copy would be incurred.

Test code:
https://coliru.stacked-crooked.com/a/96a6b4c4b69b9549
Last edited on
I swear modern C++ is a minefield.
I swear modern C++ is a minefield

Too bad there's no golden idol across the stretch to steal, ala "Raiders of the Lost Ark."
Registered users can post here. Sign in or register to post.