Как работает `void_t`

149

Я смотрел выступление Уолтера Брауна на Cppcon14 о современном программировании шаблонов ( часть I , часть II ), где он представил свою void_tтехнику SFINAE.

Пример:
дан простой шаблон переменной, который оценивает, правильно 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

http://ideone.com/HCTlBb

Вопросы:
1. Правильно ли я понимаю это?
2. Уолтер Браун заявляет, что аргумент по умолчанию должен быть того же типа, который используется void_tдля его работы. Это почему? (Я не понимаю, почему эти типы должны совпадать, разве не какой-либо тип по умолчанию работает?)

nonsensation
источник
6
Ad 2) Представьте статический утверждают , была написана как: has_member<A,int>::value. Тогда частичная специализация, которая оценивается как, has_member<A,void>не может соответствовать. Следовательно, он должен быть has_member<A,void>::valueили, с синтаксическим сахаром, аргументом по умолчанию типа void.
DYP
1
@dyp Спасибо, я отредактирую это. Ммм, я пока не вижу необходимости в has_member< T , class = void >дефолте void. Предполагая, что эта черта будет использоваться только с 1 аргументом шаблона в любое время, тогда аргумент по умолчанию может быть любого типа?
чувств
Интересный вопрос.
AStopher
2
Обратите внимание, что в этом предложении open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4436.pdf Уолтер изменился template <class, class = void>на template <class, class = void_t<>>. Так что теперь мы можем делать все, что захотим, с void_tреализацией шаблона псевдонима :)
JohnKoch

Ответы:

133

1. Основной шаблон класса

Когда вы пишете has_member<A>::value, компилятор ищет имя has_memberи находит основной шаблон класса, то есть это объявление:

template< class , class = void >
struct has_member;

(В ОП это написано как определение.)

Список аргументов шаблона <A>сравнивается со списком параметров шаблона этого первичного шаблона. Поскольку основной шаблон имеет два параметра, но поставляется только один, оставшийся параметр по умолчанию для аргумента шаблона по умолчанию: void. Это как если бы ты написал has_member<A, void>::value.

2. Специализированный шаблон класса

Теперь список параметров шаблона сравнивается с любыми специализациями шаблона 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
{ };

3. Выбор

Теперь мы можем сравнить список параметров шаблона этой специализации с аргументами шаблона, предоставленными оригиналу 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:

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

Но это не просто означает, что все параметры шаблона частичной специализации должны быть выведены; это также означает, что подстановка должна быть успешной и (как кажется?) аргументы шаблона должны соответствовать (замещенным) параметрам шаблона частичной специализации. Обратите внимание, что я не совсем понимаю, где Стандарт определяет сравнение между списком замещенных аргументов и предоставленным списком аргументов.

DYP
источник
3
Спасибо! Я читал это снова и снова, и я думаю, что мои мысли о том, как именно работает вывод аргументов шаблона и что компилятор выбирает для окончательного шаблона, на данный момент не верны.
чувств
1
@ JohannesSchaub-litb Спасибо! Это немного удручает. Неужели нет правил для сопоставления аргумента шаблона со специализацией? Даже для явных специализаций?
DYP
2
W / r / t аргументы шаблона по умолчанию, open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2008
ТК,
1
@dyp Несколько недель спустя и много читая об этом, и с подсказкой из этого фрагмента, я думаю, я начинаю понимать, как это работает. Ваше объяснение делает чтение более понятным для меня, спасибо!
чувств
1
Я хотел добавить, что ключевым термином был шаблон первичный шаблон (шаблоны впервые встречаются в коде)
сенсации
18
// 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)

Jarod42
источник