Why does the implementation of declval in libstdc++-v3 look so complicated?
Solution 1:
std::declval
is actually:
template<class T>
typename std::add_rvalue_reference<T>::type declval() noexcept;
Where std::add_rvalue_reference<T>
is usually T&&
, except in cases where that is invalid (Like if T = void
or T = int() const
), where it is just T
. The main difference is that functions cannot return arrays, but can return array references like U(&&)[]
or U(&&)[N]
.
The problem with explicitly using std::add_rvalue_reference
is that it instantiates a template. And that itself instantiates around 10s of templates at an instantiation depth of ~4 in the libstdc++ implementation. In generic code, std::declval
can be used a lot, and according to https://llvm.org/bugs/show_bug.cgi?id=27798, there is a >4% compile time boost by not using std::add_rvalue_reference
. (The libc++ implementation instantiates less templates, but it still has an impact)
This is fixed by inlining the "add_rvalue_reference
" directly into declval
. This is done using SFINAE.
The return type for declval<T>
is decltype(__declval<_Tp>(0))
. When looking up __declval
, two function templates are found.
The first has return type _Up = T&&
. The second just has return type T
.
The first takes a parameter int
, and the second long
. It is being passed 0
, which is an int
, so the first function is a better match and is chosen, and T&&
is returned.
Except, when T&&
is not a valid type (e.g., T = void
), then when the template argument _Up
is substituted with the deduced T&&
, there is a substitution failure. So it is no longer a candidate for the function. That means only the second one is left, and the 0
is converted into a long (And the return type is just T
).
In cases where T
and T&&
can't be returned from a function (e.g., T = int() const
), neither function can be picked, and the std::declval<T>
function has a substitution failure and is not a viable candidate.
Here is the libc++ commit introducing the optimisation: https://github.com/llvm/llvm-project/commit/ae7619a8a358667ea6ade5050512d0a27c03f432
And here is the libstdc++ commit: https://gcc.gnu.org/git/?p=gcc.git;a=commitdiff;h=ec26ff5a012428ed864b679c7c171e2e7d917f76
They were both previously std::add_rvalue_reference<T>::type
Solution 2:
This is to catch types where references can't be formed. In particular, void
.
Usually the int
overload is chosen. If _Tp
is void
, the int
overload will fail by _Up = void&&
, and then the long
overload is chosen.
Your implementation doesn't add references, which fails with arrays and functions.
test::declval<void()>() // fails