预期无限递归模板实例化?

Infinite recursive template instantiation expected?

我试图理解为什么一段模板元编程不会生成无限递归。我试图尽可能地减少测试用例,但仍然涉及一些设置,所以请耐心等待 :)

设置如下。我有一个通用函数 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 尾随 return 类型用于 SFINAE 目的。 foo_impl 的默认实现不定义任何调用运算符。接下来,我有一个类型特征来检测是否可以使用 T:

类型的参数调用 foo()
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 实现的类型特征的经典实现: 如果 T 存在有效的 foo_impl 专业化,has_foo<T>::value 将为真,否则为假。最后,我有两个针对整数类型和浮点类型的实现仿函数的特化:

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 都)接受以下代码:

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 上调用,编译器需要再次 select 在两个可用的 foo_impl 中进行特化;
  4. 此时,在 foo_impl 的第二次特化的启用条件下,编译器再次遇到条件 has_foo<unsigned>::value.
  5. 转到 3.

但是,代码似乎很高兴被 GCC 5.4 和 Clang 3.8 接受。看这里:http://ideone.com/XClvYT

我想了解这里发生了什么。我是不是误解了什么,递归被其他一些效果阻止了?或者我是否触发了某种 undefined/implementation 定义的行为?

其实不是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>。反过来 foo_impl.

上的 SFINAE

如果is_integral,则启用第一个。显然,这失败了。

现在考虑第二个foo_impl<float>。试图实例化它,编译看到 has_foo<unsigned>::value.

返回实例化 foo_implfoo_impl<unsigned>

第一个 foo_impl<unsigned> 匹配。

考虑第二个。 enable_if 包含 has_foo<unsigned> - 编译器已经在尝试实例化的那个。

由于当前正在实例化,因此不完整,不考虑此专业化。

递归停止,has_foo<unsigned>::value 为真,您的代码片段有效!


那么,您想知道它是如何归结到标准中的吗?好的。

[14.7.1/1] If a class template has been declared, but not defined, at the point of instantiation ([temp.point]), the instantiation yields an incomplete class type.

(不完整)

has_foo<unsigned>::value 是一个非依赖表达式,因此它会立即触发 has_foo<unsigned> 的实例化(即使从未使用过相应的特化)。

相关规则为[temp.point]/1:

For a function template specialization, a member function template specialization, or a specialization for a member function or static data member of a class template, if the specialization is implicitly instantiated because it is referenced from within another template specialization and the context from which it is referenced depends on a template parameter, the point of instantiation of the specialization is the point of instantiation of the enclosing specialization. Otherwise, the point of instantiation for such a specialization immediately follows the namespace scope declaration or definition that refers to the specialization.

(注意这里是非依赖的情况),以及 [temp.res]/8:

The program is ill-formed, no diagnostic required, if:
- [...]
- a hypothetical instantiation of a template immediately following its definition would be ill-formed due to a construct that does not depend on a template parameter, or
- the interpretation of such a construct in the hypothetical instantiation is different from the interpretation of the corresponding construct in any actual instantiation of the template.

这些规则旨在让实现自由地在上面示例中出现的地方实例化 has_foo<unsigned>,并赋予它与在此处实例化时相同的语义。 (请注意,这里的规则实际上有细微的错误:另一个实体的声明所引用的实体的实例化点实际上必须紧接在该实体之前而不是紧随其后。这是已报告为核心问题,但它尚未出现在问题列表中,因为列表已经有一段时间没有更新了。)

因此,浮点部分特化中 has_foo 的实例化点出现在该特化的声明点之前,即在部分特化的 > 之后[basic.scope.pdecl]/3:

The point of declaration for a class or class template first declared by a class-specifier is immediately after the identifier or simple-template-id (if any) in its class-head (Clause 9).

因此,当 has_foo<unsigned>foo 的调用查找 foo_impl 的偏特化时,根本找不到浮点特化。

关于您的示例的一些其他注意事项:

1) 在逗号运算符中使用 cast-to-void:

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

这是一个糟糕的模式。 operator, 查找是 still 为逗号运算符执行的,其中一个操作数是 class 或枚举类型(即使它永远不会成功)。这可能导致执行 ADL [允许执行但不需要跳过此],这会触发 return 类型的 foo 的所有关联 classes 的实例化(特别是,如果 foo returns 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 是否可以应用于 remove_reference<T> 类型的左值 即使 T 是右值引用

更好的解决方案是将整个支票放入 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 的定义之外(可能移至某些帮助程序 class 或命名空间);这样,就不需要为每次使用 has_foo.

重复实例化一次