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

blu*_*rni 10 templates sfinae c++11 c++14

我试图理解为什么一块模板元编程不会产生无限递归.我试图尽可能地减少测试用例,但仍然需要一些设置,所以忍受我:)

设置如下.我有一个泛型函数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);
}
Run Code Online (Sandbox Code Playgroud)

foo()使用decltype尾随返回类型用于SFINAE目的.默认实现foo_impl不定义任何调用操作符.接下来,我有一个type-trait,它检测是否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;
};
Run Code Online (Sandbox Code Playgroud)

这只是通过表达式SFINAE的类型特征的经典实现: has_foo<T>::value如果foo_impl存在有效的特化,则为Ttrue,否则为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) {}
};
Run Code Online (Sandbox Code Playgroud)

在最后一个foo_impl专门化,一个浮点类型,我添加了foo()必须可用于type unsigned(has_foo<unsigned>::value)的额外条件.

我不明白为什么编译器(GCC和clang都)接受以下代码:

int main()
{
    foo(1.23);
}
Run Code Online (Sandbox Code Playgroud)

在我的理解中,当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

我想了解这里发生了什么.我误解了什么,递归被其他一些影响阻止了吗?或者我可能触发某种未定义/实现定义的行为?

Ven*_*Ven 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) {}
};
Run Code Online (Sandbox Code Playgroud)

当你打电话时foo(3.14);,你实例化has_foo<float>.反过来SFINAE上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])时声明了类模板但未定义,则实例化会产生不完整的类类型.

(不完全的)


Ric*_*ith 11

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

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

对于函数模板特化,成员函数模板特化,或成员函数或类模板的静态数据成员的特化,如果特化是隐式实例化的,因为它是从另一个模板特化和其中的上下文中引用的引用依赖于模板参数,专门化的实例化点是封闭专门化的实例化点.否则,这种特化的实例化点紧跟在引用特化的命名空间范围声明或定义之后.

(注意我们在这里是非依赖的情况)和[temp.res]/8:

该程序格式错误,无需诊断,如果:
- [...]
- 由于不依赖于模板参数的构造,在其定义后立即对模板进行假设实例化,或者
-在假设实例中对这种构造的解释不同于在模板的任何实际实例化中对相应构造的解释.

这些规则旨在使实现自由地has_foo<unsigned>在上面的示例中出现的位置进行实例化,并赋予它与在那里实例化的语义相同的语义.(请注意,这里的规则实际上是错误的:由另一个实体的声明引用的实体的实例化实际上必须紧接在该实体之前,而不是紧跟在它之后.这已被报告为核心问题,但它不是问题列表,因为列表暂时没有更新.)

因此,has_foo浮点局部特化中的实例化点发生在该特化的声明点之前,该特征化是在>[basic.scope.pdecl]/3的部分特化之后:

由类说明符首先声明的类或类模板的声明点紧跟在其类头中的标识符或simple-template-id(如果有)之后(第9节).

因此,当调用foofrom has_foo<unsigned>查找部分特殊化时foo_impl,它根本找不到浮点特化.

关于你的例子的其他几点说明:

1)使用cast-to- voidin逗号运算符:

static auto test(T1 x) -> decltype(foo(x),void(),yes{});
Run Code Online (Sandbox Code Playgroud)

这是一个糟糕的模式.仍然为逗号运算符执行operator,查找,其中一个操作数是类或枚举类型(即使它永远不会成功).这可能导致执行ADL [允许实现但不需要跳过此实现],这会触发foo返回类型的所有关联类的实例化(特别是,如果返回,则可以触发实例化并且可以呈现如果该实例化不能从该翻译单元起作用,则程序格式不正确).您应该更喜欢将用户定义类型的逗号运算符的所有操作数强制转换为:foounique_ptr<X<T>>X<T>void

static auto test(T1 x) -> decltype(void(foo(x)),yes{});
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

在一般情况下,这不是正确的SFINAE模式.这里有一些问题:

  • if T是一种不能作为参数传递的类型,例如void,您触发硬错误而不是按预期进行value评估false
  • 如果T是无法形成引用的类型,则再次触发硬错误
  • 你检查是否foo可以应用于左值类型,remove_reference<T> 即使 T是左值参考

更好的解决方案是将整个检查放入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;
Run Code Online (Sandbox Code Playgroud)

这种方法也更自然地扩展到一组排名的选项:

// 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;
Run Code Online (Sandbox Code Playgroud)

最后,你的类型特征将评估更快,在编译时使用更少的内存,如果你移动的上述声明test的定义之外has_foo(或许为一些辅助类或命名空间); 这样,它们不需要为每次使用而冗余地实例化一次has_foo.