Як працює `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для його роботи. Чому так? (Я не бачу, чому потрібно відповідати цим типам, чи не спрацьовує будь-який тип за замовчуванням?)


6
Ad 2) Уявіть статичний стверджують , була написана як: has_member<A,int>::value. Тоді часткова спеціалізація, за якою оцінюється, не has_member<A,void>може відповідати. Тому він повинен бути has_member<A,void>::value, або з синтаксичним цукром, аргументом типу за замовчуванням void.
dyp

1
@dyp Спасибі, я це відредагую. Mh, я ще не бачу потреби в has_member< T , class = void >дефолті void. Якщо припустити, що ця ознака буде використовуватися лише з 1 аргументом шаблону в будь-який час, тоді аргумент за замовчуванням може бути будь-якого типу?
безглуздя

Цікаве запитання.
AStopher

2
Зауважте, що у цій пропозиції відкрите-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>. Аргументи не відповідають параметрам спеціалізації, і основний шаблон вибирається як запасний.


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

Часткова спеціалізація відповідає заданому фактичному списку аргументів шаблону, якщо аргументи шаблону часткової спеціалізації можна вивести із фактичного списку аргументів шаблону.

Але це не просто означає , що всі шаблонні-параметри часткової спеціалізації повинні бути виведені; це також означає, що підміна повинна мати успіх і (як здається?) аргументи шаблону повинні відповідати (заміщеним) параметрам шаблону часткової спеціалізації. Зауважте, що я не повністю знаю, де Стандарт вказує порівняння між заміненим списком аргументів та наданим списком аргументів.


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)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.