Как работает `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 > с void как аргумент по умолчанию
  • has_member< B > оценивается как false_type

http://ideone.com/HCTlBb

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

Ответ 1

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

template< class , class = void >
struct has_member;

(В OP, написанном как определение.)

Список аргументов шаблона <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>. Аргументы не соответствуют параметрам специализации, а основной шаблон выбирается как спад.


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

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

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

Ответ 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> (поэтому мы используем определение по умолчанию: inherit from false_type)