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

#c #template-meta-programming #sfinae #void-t

Вопрос:

Я смотрел выступление Уолтера Брауна на 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 > с пустым аргументом по умолчанию
  • has_member< B > оценивает, чтобы false_type

http://ideone.com/HCTlBb

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

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

1. Объявление 2) Представьте, что статическое утверждение было написано следующим образом: has_member<A,int>::value . Затем частичная специализация, которая оценивается has_member<A,void> как не может совпадать. Следовательно, он должен быть has_member<A,void>::value или , с синтаксическим сахаром, аргументом типа по умолчанию void .

2. @dyp Спасибо, я отредактирую это. Мх, я пока не вижу необходимости в has_member< T , class = void > том, чтобы объявлять дефолт void . Предполагая, что эта черта будет использоваться только с 1 аргументом шаблона в любое время, то аргумент по умолчанию может быть любого типа?

3. Интересный вопрос.

4. Обратите внимание, что в этом предложении, open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4436.pdf , Уолтер переоделся template <class, class = void> в template <class, class = void_t<>> . Так что теперь мы можем делать все, что захотим, с void_t реализацией шаблона псевдонима 🙂

Ответ №1:

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 constamp; , когда мы все еще могли бы сделать вывод T . Для шаблона T и аргумента шаблона A мы выводим T A , что это так .

Во втором шаблоне void_t< decltype( T::member ) > параметр T шаблона отображается в контексте, где он не может быть выведен из какого-либо аргумента шаблона.

Для этого есть две причины:

  • Выражение внутри decltype явно исключается из вычета аргумента шаблона. Я думаю, это потому, что это может быть сколь угодно сложным.
  • Даже если мы использовали шаблон без decltype like 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> . Аргументы не соответствуют параметрам специализации, и в качестве запасного варианта выбирается основной шаблон.


(*) Стандарт, ИМХО, сбивает с толку, включает процесс подстановки и сопоставление явно указанных аргументов шаблона в процессе вывода аргументов шаблона. Например (после N4296) [соответствие temp.class.spec.]/2:

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

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

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

1. Спасибо! Я читал это снова и снова, и я думаю, что мое представление о том, как именно работает вывод аргументов шаблона и что компилятор выбирает для окончательного шаблона, на данный момент неверно.

2. @JohannesSchaub-litb Спасибо! Хотя это немного удручает. Действительно ли нет правил для сопоставления аргумента шаблона со специализацией? Даже для явных специализаций?

3. Без аргументов шаблона по умолчанию, open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2008

4. @dyp Несколько недель спустя, много читая об этом и с подсказкой из этого фрагмента , я думаю, что начинаю понимать, как это работает. Ваше объяснение имеет для меня больше смысла от чтения к чтению, спасибо!

5. Я хотел добавить, что ключевым был термин » основной шаблон» (шаблоны, впервые встречающиеся в коде).

Ответ №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 )

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

1. Итак void_t<decltype(T::member)>> , это не что иное, как стандартизированный/более понятный способ написания decltype(T::member, void()) ?

2. @303: некоторые (старые) версии компилятора испытывали трудности с применением SFINAE с (некоторыми реализациями) void_t , хотя.