Почему реализация declval в libstdC++ - v3 выглядит такой сложной?
Код ниже взят из libstdC++-v3 std::type_traits
, который является реализацией
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));
Но я думаю, что смогу реализовать
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
- Почему реализация в libstdC++-v3 выглядит такой сложной?
- Что делает шаблон 1 в первом фрагменте?
- Почему
__declval
нужен параметр (int
/long
)? - Почему шаблон 1 (
int
) и шаблон 2 (long
) имеют разные типы параметров? - Есть ли проблемы с моей простой реализацией?
2 ответа
std::declval
на самом деле:
template<class T>
typename std::add_rvalue_reference<T>::type declval() noexcept;
где std::add_rvalue_reference<T>
обычно
T&&
, за исключением случаев, когда это недопустимо (например, если
T = void
или же
T = int() const
), где это просто
T
. Основное отличие состоит в том, что функции не могут возвращать массивы, но могут возвращать ссылки на массивы, например
U(&&)[]
или же
U(&&)[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>
является
decltype(__declval<_Tp>(0))
. Глядя вверх
__declval
, найдено два шаблона функций.
Первый имеет тип возврата
_Up = T&&
. У второго просто есть возвращаемый тип
T
.
Первый принимает параметр
int
, а второй
long
. Это проходит
0
, что является
int
, поэтому первая функция лучше подходит и выбрана, и
T&&
возвращается.
Кроме случаев, когда
T&&
не является допустимым типом (например,
T = void
), тогда, когда аргумент шаблона
_Up
заменяется выведенным
T&&
, произошла ошибка замены. Таким образом, он больше не является кандидатом на эту функцию. Это означает, что остался только второй, а
0
преобразуется в длинное (а возвращаемый тип просто
T
).
В случаях, когда
T
и
T&&
не может быть возвращено из функции (например,
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=commitdiff;h=ec26ff5a012428ed864b679c7c171e2e7d917f76
Они оба были раньше
std::add_rvalue_reference<T>::type
Это нужно для того, чтобы поймать типы, на которые невозможно сформировать ссылки. В частности,
void
.
Обычно
int
выбрана перегрузка. Если
_Tp
является
void
, то
int
перегрузка не удастся
_Up = void&&
, а затем
long
выбрана перегрузка.
Ваша реализация не добавляет ссылок, что не работает с массивами и функциями.
test::declval<void()>() // fails