#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
Вопросы:
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
likevoid_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
, хотя.