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

时间:2016-08-20 23:12:20

标签: templates c++11 c++14 sfinae

我试图理解为什么一块模板元编程生成无限递归。我试图尽可能地减少测试用例,但是还有一些设置,所以忍受我:)

设置如下。我有一个泛型函数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将为true,否则为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。
  6. 然而,似乎GCC 5.4和Clang 3.8都很乐意接受这些代码。见这里:http://ideone.com/XClvYT

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

2 个答案:

答案 0 :(得分: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>。这反过来又是foo_impl上的SFINAE。

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

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

返回实例化foo_implfoo_impl<unsigned>

第一个foo_impl<unsigned>是匹配。

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

由于它目前正在实例化,因此不完整,并且不考虑此专业化。

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

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

  

[14.7.1 / 1]如果在实例化([temp.point])时声明了一个类模板但未定义,则实例化会产生一个不完整的类类型。

(不完全的)

答案 1 :(得分:11)

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

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

  

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

(请注意,我们在这里是非依赖案例)和[temp.res] / 8:

  

该计划是   形成不良,无需诊断,如果:
    - [...]
    - 由于不依赖于模板参数的构造,或者由于不依赖于模板参数的构造,因此在定义后立即对模板进行假设实例化会形成不良     - 在假设实例中对这种构造的解释不同于在模板的任何实际实例化中对相应构造的解释。

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

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

  

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

因此,当foohas_foo<unsigned>的调用查找foo_impl的部分特殊情况时,它根本找不到浮点专精。

关于您的示例的其他几点说明:

1)在逗号运算符中使用强制转换 - 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 < / LI>
  • 如果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;

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

// 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的每次使用,它们不需要冗余地实例化一次。