Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the implementation of declval in libstdc++-v3 look so complicated?

The code below comes from libstdc++-v3 std::type_traits, which is an implementation of std::declval:

  template<typename _Tp, typename _Up = _Tp&&> // template 1
    _Up
    __declval(int);
  template<typename _Tp> // template 2
    _Tp
    __declval(long);
  template<typename _Tp> // template 3
    auto declval() noexcept -> decltype(__declval<_Tp>(0));

But I think I can implement declval as simply:

template <typename T> T declval();

Here is my test code:

#include <iostream>
using namespace std;

struct C {
    C() = delete;
    int foo() { return 0; }
};

namespace test {
template <typename T> T declval();
};// namespace test

int main() {
    decltype(test::declval<C>().foo()) n = 1;
    cout << n << endl;
}

Build and run commands are:

g++ -std=c++11 ./test.cpp
./a.out
  1. Why does the implementation in libstdc++-v3 look so complicated?
  2. What does template 1 in the first snippet do?
  3. Why does __declval need a parameter (int/long)?
  4. Why do template 1 (int) and template 2 (long) have different parameter types?
  5. Are there any problems with my simple implementation?
like image 682
expoter Avatar asked Sep 25 '20 08:09

expoter


2 Answers

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

like image 75
Artyer Avatar answered Oct 18 '22 20:10

Artyer


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
like image 34
Passer By Avatar answered Oct 18 '22 21:10

Passer By