Почему реализация declval в libstdc -v3 выглядит такой сложной?

#c #c 11 #templates #decltype #declval

#c #c 11 #шаблоны #decltype #declval

Вопрос:

Приведенный ниже код взят из libstdc -v3 std::type_traits , который является реализацией std::declval :

   template<typename _Tp, typename _Up = _Tpamp;amp;> // 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));
  

Но я думаю, что я могу реализовать declval так просто:

 template <typename T> T declval();
  

Вот мой тестовый код:

 #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;
}
  

Команды сборки и запуска:

 g   -std=c  11 ./test.cpp
./a.out
  
  1. Почему реализация в libstdc -v3 выглядит такой сложной?
  2. Что делает шаблон 1 в первом фрагменте?
  3. Зачем __declval нужен параметр ( int / long )?
  4. Почему template 1 ( int ) и template 2 ( long ) имеют разные типы параметров?
  5. Есть ли какие-либо проблемы с моей простой реализацией?

Ответ №1:

std::declval на самом деле:

 template<class T>
typename std::add_rvalue_reference<T>::type declval() noexcept;
  

Где std::add_rvalue_reference<T> обычно Tamp;amp; , за исключением случаев, когда это недопустимо (например, if T = void или T = int() const ), где это просто T . Основное отличие состоит в том, что функции не могут возвращать массивы, но могут возвращать ссылки на массивы, такие как U(amp;amp;)[] или U(amp;amp;)[N] .

Проблема с явным использованием std::add_rvalue_reference заключается в том, что он создает экземпляр шаблона. И это само по себе создает экземпляры около 10 шаблонов с глубиной создания ~ 4 в реализации libstdc . В общем коде std::declval может использоваться много, и в соответствии с https://llvm.org/bugs/show_bug.cgi?id=27798 , при неиспользовании время компиляции увеличивается более чем на 4% std::add_rvalue_reference . (Реализация libc создает меньше шаблонов, но это все равно оказывает влияние)

Это исправлено путем встраивания « add_rvalue_reference » непосредственно в declval . Это делается с помощью SFINAE.


Возвращаемый тип для declval<T> is decltype(__declval<_Tp>(0)) . При поиске обнаруживаются __declval два шаблона функций.

Первый имеет возвращаемый тип _Up = Tamp;amp; . Второй просто имеет возвращаемый тип T .

Первый принимает параметр int , а второй long . Он передается 0 , что является an int , поэтому первая функция лучше подходит, выбирается и Tamp;amp; возвращается.

За исключением случаев, когда Tamp;amp; недопустимый тип (например, T = void ), тогда, когда аргумент шаблона _Up заменяется на выведенный Tamp;amp; , происходит сбой замены. Так что это больше не кандидат на функцию. Это означает, что остается только второй, и 0 преобразуется в long (а возвращаемый тип — just T ).

В случаях, когда T и Tamp;amp; не может быть возвращен из функции (например, T = int() const ), ни одна из функций не может быть выбрана, а std::declval<T> функция имеет ошибку замены и не является жизнеспособным кандидатом.


Вот коммит libc , представляющий оптимизацию: https://github.com/llvm/llvm-project/commit/ae7619a8a358667ea6ade5050512d0a27c03f432

И вот фиксация libstdc : https://gcc.gnu.org/git/?p=gcc.git ;a=committdiff;h=ec26ff5a012428ed864b679c7c171e2e7d917f76

Они оба были ранее std::add_rvalue_reference<T>::type

Комментарии:

1. Но… почему второй параметр шаблона с аргументом по умолчанию, а не _Tpamp;amp; ?

2. @JohannesSchaub-litb, похоже, не является какой-либо конкретной причиной. libstdc реализует его с _Tpamp;amp; возвращаемым типом. Остальные признаки типа в libc используют неназванный bool = SFINAE_stuff or typename = SFINAE_stuff , так что, может быть, для согласованности? В любом случае это не имеет значения

Ответ №2:

Это делается для перехвата типов, в которых ссылки не могут быть сформированы. В частности, void .

Обычно int выбирается перегрузка. Если _Tp есть void , int перегрузка завершится ошибкой _Up = voidamp;amp; , а затем long выбирается перегрузка.

Ваша реализация не добавляет ссылки, что приводит к сбою с массивами и функциями.

 test::declval<void()>() // fails