Как работает `void_t`
Я смотрел выступление Уолтера Брауна на Cppcon14 о современном программировании шаблонов ( часть I, часть II), где он представил свою void_t
СФИНАЕ техника.
Пример:
Учитывая простой шаблон переменной, который оценивает void
если все аргументы шаблона правильно сформированы:
template< class ... > using void_t = void;
и следующая черта, которая проверяет существование переменной- члена, называемой member:
template< class , class = void >
struct has_member : std::false_type
{ };
// specialized as has_member< T , void > or discarded (sfinae)
template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : std::true_type
{ };
Я пытался понять, почему и как это работает. Поэтому крошечный пример:
class A {
public:
int member;
};
class B {
};
static_assert( has_member< A >::value , "A" );
static_assert( has_member< B >::value , "B" );
1. has_member< A >
has_member< A , void_t< decltype( A::member ) > >
A::member
существуетdecltype( A::member )
хорошо сформированvoid_t<>
действителен и оцениваетvoid
has_member< A , void >
и поэтому он выбирает специализированный шаблонhas_member< T , void >
и оцениваетtrue_type
2. has_member< B >
has_member< B , void_t< decltype( B::member ) > >
B::member
не существуетdecltype( B::member )
плохо сформирован и терпит неудачу (sfinae)has_member< B , expression-sfinae >
так что этот шаблон отбрасывается
- компилятор находит
has_member< B , class = void >
с void в качестве аргумента по умолчанию has_member< B >
оцениваетfalse_type
Вопросы:
1. Правильно ли мое понимание этого?
2. Уолтер Браун утверждает, что аргумент по умолчанию должен быть того же типа, что и используемый в void_t
чтобы это работало. Это почему? (Я не понимаю, почему эти типы должны совпадать, разве не какой-либо тип по умолчанию работает?)
3 ответа
Когда ты пишешь has_member<A>::value
, компилятор ищет имя has_member
и находит основной шаблон класса, то есть это объявление:
template< class , class = void >
struct has_member;
(В ОП это написано как определение.)
Список аргументов шаблона <A>
сравнивается со списком параметров шаблона этого основного шаблона. Поскольку основной шаблон имеет два параметра, но вы указали только один, для оставшегося параметра по умолчанию используется аргумент шаблона по умолчанию: void
, Это как если бы вы написали has_member<A, void>::value
,
Теперь список параметров шаблона сравнивается с любыми специализациями шаблона. has_member
, Только если никакая специализация не соответствует, определение основного шаблона используется как запасной вариант. Так что частичная специализация учитывается:
template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };
Компилятор пытается сопоставить аргументы шаблона A, void
с образцами, определенными в частичной специализации: T
а также void_t<..>
по одному. Сначала выполняется вывод аргумента шаблона. Приведенная выше частичная специализация по-прежнему представляет собой шаблон с параметрами-шаблонами, которые должны быть "заполнены" аргументами.
Первый шаблон, T
, позволяет компилятору выводить параметр шаблона T
, Это тривиальный вывод, но рассмотрим такую модель, как T const&
где мы могли бы еще вывести T
, Для шаблона T
и аргумент шаблона A
мы выводим T
быть A
,
Во втором паттерне void_t< decltype( T::member ) >
, шаблон-параметр T
появляется в контексте, где это не может быть выведено из любого аргумента шаблона. Для этого есть две причины:
Выражение внутри
decltype
явно исключен из вывода аргумента шаблона. Я думаю, это потому, что это может быть сколь угодно сложным.Даже если бы мы использовали шаблон без
decltype
лайкvoid_t< T >
, то вычетT
происходит на разрешенном шаблоне псевдонима. То есть мы разрешаем шаблон псевдонима, а затем пытаемся определить типT
из полученной картины. Результирующий образец однакоvoid
, который не зависит отT
и, следовательно, не позволяет нам найти конкретный тип дляT
, Это похоже на математическую проблему попытки инвертировать постоянную функцию (в математическом смысле этих терминов).
Вывод аргументов шаблона завершен (*), теперь выводимые аргументы шаблона заменяются. Это создает специализацию, которая выглядит следующим образом:
template<>
struct has_member< A, void_t< decltype( A::member ) > > : true_type
{ };
Тип void_t< decltype( A::member ) > >
Теперь можно оценить. Он хорошо сформирован после замещения, следовательно, сбоев замещения не происходит. Мы получаем:
template<>
struct has_member<A, void> : true_type
{ };
Теперь мы можем сравнить список параметров шаблона этой специализации с аргументами шаблона, предоставленными оригиналу has_member<A>::value
, Оба типа точно совпадают, поэтому эта частичная специализация выбрана.
С другой стороны, когда мы определяем шаблон как:
template< class , class = int > // <-- int here instead of void
struct has_member : false_type
{ };
template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };
Мы заканчиваем с той же специализацией:
template<>
struct has_member<A, void> : true_type
{ };
но наш список аргументов шаблона для has_member<A>::value
сейчас <A, int>
, Аргументы не соответствуют параметрам специализации, и основной шаблон выбирается как запасной вариант.
(*) Стандарт, ИМХО сбивает с толку, включает процесс замены и сопоставление явно определенных аргументов шаблона в процессе вывода аргументов шаблона. Например (post-N4296) [temp.class.spec.match]/2:
Частичная специализация соответствует заданному фактическому списку аргументов шаблона, если аргументы шаблона частичной специализации могут быть выведены из фактического списка аргументов шаблона.
Но это не просто означает, что все параметры шаблона частичной специализации должны быть выведены; это также означает, что подстановка должна быть успешной и (как кажется?) аргументы шаблона должны соответствовать (замещенным) параметрам шаблона частичной специализации. Обратите внимание, что я не совсем понимаю, где Стандарт определяет сравнение между списком замещенных аргументов и предоставленным списком аргументов.
// specialized as has_member< T , void > or discarded (sfinae)
template<class T>
struct has_member<T , void_t<decltype(T::member)>> : true_type
{ };
Эта специализация существует только тогда, когда она хорошо сформирована, поэтому, когда decltype( T::member )
является действительным и не двусмысленным. специализация так для has_member<T , void>
как говорится в комментарии.
Когда ты пишешь has_member<A>
, это has_member<A, void>
из-за аргумента шаблона по умолчанию.
И у нас есть специализация для has_member<A, void>
(так наследовать от true_type
) но у нас нет специализации для has_member<B, void>
(поэтому мы используем определение по умолчанию: унаследовать от false_type
)
Этот тред и тред SFINAE: Понимание void_t и detect_if спасли меня. Я хочу продемонстрировать поведение на нескольких примерах:
Инструмент: cppinsights
Протестировать реализацию по типам float и следующим типам:
struct A {
using type = int;
};
struct B{
using type = void;
}
Протестировано
auto f = has_type_member<float>::value;
auto a = has_type_member<A>::value;
auto b = has_type_member<B>::value;
стандартная реализация
#include <type_traits>
// primary template handles types that have no nested ::type member:
template< class, class = int >
struct has_type_member : std::false_type { };
// specialization recognizes types that do have a nested ::type member:
template< class T >
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { };
Выход
bool f = false;
bool a = true;
bool b = true;
bool x = has_type_member<A, int>::value; //x = false;
Случай 1
#include <type_traits>
// primary template handles types that have no nested ::type member:
template< class, class = int >
struct has_type_member : std::false_type { };
template< class T >
struct has_type_member<T, void> : std::true_type { };
// specialization recognizes types that do have a nested ::type member:
template< class T >
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { };
Выход
/home/insights/insights.cpp:14:8: error: redefinition of 'has_type_member<T, std::void_t<typename T::type>>'
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { };
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/insights/insights.cpp:8:8: note: previous definition is here
struct has_type_member<T, void>: std::true_type {};
^
1 error generated.
Error while processing /home/insights/insights.cpp.
Так,
has_type_member<T, std::void_t<typename T::type>>
определил специализацию
has_type_member
и подпись точно.
случай 2
#include <type_traits>
template< class, class = void >
struct has_type_member : std::false_type { };
// specialize 2nd type as void
template< class T>
struct has_type_member<T, void> : std::true_type { };
Выход:
bool f = true;
bool a = true;
bool b = true;
Этот случай показывает, что компилятор:
- Хотел найти пару
- Выяснил, что шаблон требует 2 аргумента, затем заполнил 2-й аргумент аргументами по умолчанию. Структура была похожа
- Нашел специализацию этой подписи и получил значение от
std::true_type
случай 3
#include <type_traits>
template< class, class = void >
struct has_type_member : std::false_type { };
template<class T>
struct has_type_member<T, typename T::type>: std::true_type {};
Выход:
bool f = false;
bool a = false;
bool b = true;
случай f
-
has_type_member<float>
был завершен вhas_type_member<float, void>
. - Затем компилятор попытался
typename float::type
и потерпел неудачу. - Выбран основной шаблон.
случай а
-
has_type_member<A>
был завершен в - Затем компилятор попытался
has_type_member<A, typename A::type>
и узнал, что этоhas_type_member<A, int>
- Компилятор решил, что это не спецификация
has_type_member<A, void>
- Затем выбран первичный шаблон.
случай б
-
has_type_member<B>
был завершен в . - Затем компилятор попытался
has_type_member<B, typename B::type>
и узнал, что это было. - Компилятор решил, что это спецификация
has_type_member<B, void>
- выбрал.
случай 4
#include <type_traits>
//int as default 2nd argument
template< class, class = int >
struct has_type_member : std::false_type { };
template<class T>
struct has_type_member<T, std::void<typename T::type>>: std::true_type {};
Выход:
bool f = false;
bool a = false;
bool b = false;
The
has_type_member<T>
имеет тип
has_type_member<T, int>
для всех трех переменных, а
true_type
имеет подпись как
has_type_member<T, void>
если это действительно.
Вывод
Итак
std::void_t
:
- Проверить, если
T::type
действительный. - Предоставляет специализацию основного шаблона, если предоставлен только один аргумент шаблона.