2016-08-20 2 views
10

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

Настройка следующая. У меня есть общие функции foo(T) который делегирует осуществление к родовому функтора под названием foo_impl через его оператор вызова, например:

template <typename T, typename = void> 
struct foo_impl {}; 

template <typename T> 
inline auto foo(T x) -> decltype(foo_impl<T>{}(x)) 
{ 
    return foo_impl<T>{}(x); 
} 

foo() использует decltype отставая тип возвращаемого значения для SFINAE целей. Реализация по умолчанию foo_impl не определяет оператор вызова. Далее, у меня есть тип-признак, который определяет, может ли foo() быть вызвана с аргументом типа T:

template <typename T> 
struct has_foo 
{ 
    struct yes {}; 
    struct no {}; 
    template <typename T1> 
    static auto test(T1 x) -> decltype(foo(x),void(),yes{}); 
    static no test(...); 
    static const bool value = std::is_same<yes,decltype(test(std::declval<T>()))>::value; 
}; 

Это просто классическая реализация типа признака с помощью выражения SFINAE: has_foo<T>::value будет справедливо, если действующий номер foo_impl специализация есть для T, false в противном случае. Наконец, у меня есть два специализаций функтора реализации для целочисленных типов и для типов с плавающей точкой:

template <typename T> 
struct foo_impl<T,typename std::enable_if<std::is_integral<T>::value>::type> 
{ 
    void operator()(T) {} 
}; 

template <typename T> 
struct foo_impl<T,typename std::enable_if<has_foo<unsigned>::value && std::is_floating_point<T>::value>::type> 
{ 
    void operator()(T) {} 
}; 

В последней foo_impl специализации, один для типов с плавающей точкой, я добавил дополнительное условие, что foo() должны быть доступны для типа unsigned (has_foo<unsigned>::value).

То, что я не понимаю, почему составители (GCC & лязг оба) принимает следующий код:

int main() 
{ 
    foo(1.23); 
} 

В моем понимании, когда foo(1.23) называется следующее должно произойти:

  1. специализация foo_impl для интегральных типов отбрасывается, поскольку 1.23 не является неотъемлемой частью, поэтому рассматривается только вторая специализация foo_impl;
  2. условие включения второй специализации foo_impl содержит has_foo<unsigned>::value, то есть компилятор должен проверить, можно ли вызывать foo() по типу unsigned;
  3. для того, чтобы узнать, может ли foo() назвать тип unsigned, компилятору необходимо еще раз выбрать специализацию foo_impl среди двух доступных;
  4. В этом вопросе в условиях включения второй специализации foo_impl компилятор снова встречает условие has_foo<unsigned>::value.
  5. GOTO 3.

Однако, похоже, что код с радостью согласился и на GCC 5.4 и Clang 3.8. См. Здесь: http://ideone.com/XClvYT

Я хотел бы понять, что здесь происходит. Я что-то не понимаю и рекурсия блокируется каким-то другим эффектом? Или, может быть, я запускаю какое-то неопределенное/определенное поведение?

+0

Попробуйте изменить беззнаковое в вашем примере, чтобы плавать и посмотреть, что произойдет. После этого происходит смена замены. unsigned будет оцениваться по is_integral, чтобы быть правдой. Итак, я думаю, что он сначала проверяет второй foo_impl, тогда первый из них важен. – rtbaldwin

+0

[Бесконечная рекурсия в создании шаблона - неопределенное поведение.] (Http://eel.is/c++draft/temp.inst#14) –

ответ

11

has_foo<unsigned>::value не является зависимым выражение, поэтому он сразу же вызывает инстанцирование has_foo<unsigned> (даже если соответствующая специализация никогда не используется).

Соответствующие правила [temp.point]/1:

Для шаблона функции специализации, функции член специализации шаблона, или специализации функции члена или статического члена данных шаблона класса, если специализация неявно создается, потому что на нее ссылаются из другой специализированной специализации, а контекст, с которой она ссылается, зависит от параметра шаблона, точкой инстанцирования специализации является точка инстанцирования охватывающей специализации. В противном случае точка инстанцирования для такой специализации немедленно следует за объявлением или определением области пространства имен, которое относится к специализации.

(обратите внимание, что мы находимся здесь в независящем случае) и [temp.Рез]/8:

Программа плохо формируется, нет диагностики требуется, если:
- [...]
- гипотетическая конкретизация шаблона сразу после его определения был бы жестоким образованный из-за конструкции, которая не зависит от параметра шаблона, или
- интерпретация такой конструкции в гипотетическом экземпляре отличается от интерпретации соответствующей конструкции в любой фактической экземпляре шаблона.

Эти правила призваны дать свободу реализации для создания экземпляра has_foo<unsigned> в точке, где она появляется в приведенном выше примере, и дать ему ту же семантику, как если бы она была инстанцирован там. (Обратите внимание, что правила здесь на самом деле неверно: точка экземпляра для объекта, на который ссылается объявление другого объекта , фактически должна немедленно предшествовать этому объекту, а не сразу после него. Это было сообщено как основная проблема, но это а не в списке проблем, так как список не обновлялся некоторое время.)

Как следствие, точка создания has_foo в частичной специализации с плавающей запятой происходит до момента объявления этой специализации, который после > частичной специализации на [basic.scope.pdecl]/3:

Точка объявления для шаблона класса или класса, сначала объявленного спецификатором класса, сразу после идентификатора или простого шаблона-идентификатора (если есть) в его классе-голове (раздел 9).

Поэтому, когда вызов foo из has_foo<unsigned> смотрит парциальное specializatios из foo_impl, он не находит специализацию с плавающей точкой на всех.

Несколько других заметок о вашем примере:

1) Использование монолитного TO- void в оператор запятая:

static auto test(T1 x) -> decltype(foo(x),void(),yes{}); 

Это плохая картина. operator, lookup is все еще выполняется для оператора запятой, где один из его операндов имеет тип класса или перечисления (хотя он никогда не сможет добиться успеха). Это может привести к выполнению ADL [реализация разрешена, но не требуется пропустить это), которая запускает создание всех ассоциированных классов возвращаемого типа foo (в частности, если foo возвращает unique_ptr<X<T>>, это может инициировать создание X<T> и может сделать программу плохо сформированной, если эта копия не работает из этой единицы перевода). Вы должны предпочесть, чтобы бросить все операнды запятой оператора определяемого пользователем типа для void:

static auto test(T1 x) -> decltype(void(foo(x)),yes{}); 

2) SFINAE идиома:

template <typename T1> 
static auto test(T1 x) -> decltype(void(foo(x)),yes{}); 
static no test(...); 
static const bool value = std::is_same<yes,decltype(test(std::declval<T>()))>::value; 

Это не правильная модель SFINAE в общем случае.Есть несколько проблем здесь:

  • если T является типом, который не может быть передан в качестве аргумента, например void, вы вызвать тяжелую ошибку вместо value вычисляемой false по назначению
  • если T является типа, к которому ссылка не может быть сформирована, вы снова вызвать тяжелую ошибку
  • вы проверить, может ли foo применяться к Lvalue типа remove_reference<T>даже еслиT является ссылкой Rvalue

Лучшее решение поместить всю проверку в версии testyes вместо разделения declval часть в value:

template <typename T1> 
static auto test(int) -> decltype(void(foo(std::declval<T1>())),yes{}); 
template <typename> 
static no test(...); 
static const bool value = std::is_same<yes,decltype(test<T>(0))>::value; 

Этот подход более естественным образом распространяется на ранжированный набор опций:

// elsewhere 
template<int N> struct rank : rank<N-1> {}; 
template<> struct rank<0> {}; 


template <typename T1> 
static no test(rank<2>, std::enable_if_t<std::is_same<T1, double>::value>* = nullptr); 
template <typename T1> 
static yes test(rank<1>, decltype(foo(std::declval<T1>()))* = nullptr); 
template <typename T1> 
static no test(rank<0>); 
static const bool value = std::is_same<yes,decltype(test<T>(rank<2>()))>::value; 

И, наконец, ваш тип признака будет быстрее оцениваться и использовать меньше памяти во время компиляции, если вы переместите вышеприведенные объявления test вне определения has_foo (возможно, в некоторый класс помощника или пространство имен); Таким образом, им не нужно резервировать один раз для каждого использования has_foo.

+0

Спасибо, что ответили на мой звонок! :) – Ven

+0

Ах! Я даже не стал пытаться найти правила для того, видна ли специализация (и вместо этого преследовала гипотеза о неполноте), потому что я полностью пропустил последствия того, когда создаются именно независимые шаблонные классы. Спасибо за просветление! – Griwes

+0

... также у меня теперь странное чувство, что все это означает, что это в основном покрыто лимериком, что было бы еще одним вещью, которую я полностью проигнорировал при чтении стандартного вчера. Мне стыдно. – Griwes

11

На самом деле это не UB. Но это действительно показывает вам, как TMP является сложным ...

Причина, по которой это не бесконечно рекурсивно, объясняется полнотой.

template <typename T> 
struct foo_impl<T,typename std::enable_if<std::is_integral<T>::value>::type> 
{ 
    void operator()(T) {} 
}; 

// has_foo here 

template <typename T> 
struct foo_impl<T,typename std::enable_if<has_foo<unsigned>::value && std::is_floating_point<T>::value>::type> 
{ 
    void operator()(T) {} 
}; 

Когда вы звоните foo(3.14);, инстанцировании has_foo<float>. Это в свою очередь SFINAEs на foo_impl.

Первый разрешен, если is_integral. Очевидно, это не удается.

В настоящее время рассматривается второй foo_impl<float>. Пытаясь создать экземпляр, компиляторы видят has_foo<unsigned>::value.

Назад к созданию экземпляра foo_impl: foo_impl<unsigned>!

Первый foo_impl<unsigned> - это матч.

Рассматривается второй. enable_if содержит has_foo<unsigned> - тот, который уже пытается создать экземпляр компилятора.

Поскольку в настоящее время создается экземпляр, это неполный, и эта специализация не рассматривается.

Рекурсивные остановки, has_foo<unsigned>::value - это правда, и ваш фрагмент кода работает!


Итак, вы хотите знать, как это доходит до него в стандарте? Хорошо.

[14.7.1/1] Если шаблон шаблона был объявлен, но не определен, в момент создания экземпляра ([temp.point]), экземпляр дает неполный тип класса.

(неполный)

+0

Большое спасибо за объяснение. Есть ли у вас какая-либо ссылка на соответствующий стандартный раздел для изучения? Или ссылку cppreference или что-то в этом роде :) – bluescarni

+0

Uhhh. Я собираюсь забыть об основном факте, что вы пытаетесь убить меня, и попытайтесь извлечь для себя эту легализацию ... – Ven

+0

Даже небольшой указатель? :) Я не совсем понимаю 2 вещи: 1) когда вы говорите, что это «неполное», вы имеете в виду как в «неполном типе»? 2) неполнота вызывает специализацию SFINAE или есть какой-то другой механизм на работе? – bluescarni

Смежные вопросы