Ожидается создание бесконечного рекурсивного шаблона?

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

Настройка следующая. У меня есть общая функция 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 будет истинным, если для T существует действительная специализация foo_impl, иначе 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 и clang both) принимают следующий код:

int main()
{
    foo(1.23);
}

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

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

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

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

Ответ 1

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

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

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

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

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

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

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

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

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

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

1) Использование cast-to- void в операторе запятой:

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

Это плохой шаблон. operator, поиск по-прежнему выполняется для оператора запятой, где один из его операндов имеет тип класса или перечисления (хотя он никогда не сможет добиться успеха). Это может привести к выполнению 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

Лучшее решение состоит в том, чтобы поместить всю проверку в версию yes test вместо разделения фрагмента 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 им не требуется избыточно создавать экземпляры один раз.

Ответ 2

Это не на самом деле 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]), экземпляр дает неполный тип класса.

(неполные)