Как работает `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

http://ideone.com/HCTlBb

Вопросы:
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;

стандартная реализация

Из этой ссылки std::void_t

      #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;

Этот случай показывает, что компилятор:

  1. Хотел найти пару
  2. Выяснил, что шаблон требует 2 аргумента, затем заполнил 2-й аргумент аргументами по умолчанию. Структура была похожа
  3. Нашел специализацию этой подписи и получил значение от 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

  1. has_type_member<float>был завершен в has_type_member<float, void>.
  2. Затем компилятор попытался typename float::typeи потерпел неудачу.
  3. Выбран основной шаблон.

случай а

  1. has_type_member<A>был завершен в
  2. Затем компилятор попытался has_type_member<A, typename A::type>и узнал, что это has_type_member<A, int>
  3. Компилятор решил, что это не спецификация has_type_member<A, void>
  4. Затем выбран первичный шаблон.

случай б

  1. has_type_member<B>был завершен в .
  2. Затем компилятор попытался has_type_member<B, typename B::type>и узнал, что это было.
  3. Компилятор решил, что это спецификация has_type_member<B, void>
  4. выбрал.

случай 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:

  1. Проверить, если T::typeдействительный.
  2. Предоставляет специализацию основного шаблона, если предоставлен только один аргумент шаблона.
Другие вопросы по тегам