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

Mar 3, 2024 at 8:40pm
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 Mar 3, 2024 at 8:40pm
Mar 4, 2024 at 7:05am
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 Mar 4, 2024 at 10:39pm
Mar 4, 2024 at 8:08pm
I swear modern C++ is a minefield.
Mar 5, 2024 at 1:57am
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."
Topic archived. No new replies allowed.