哪个是更专业的模板功能? clang 和 g++ 对此有所不同

Posted

技术标签:

【中文标题】哪个是更专业的模板功能? clang 和 g++ 对此有所不同【英文标题】:Which is the more specialized template function? clang and g++ differ on that 【发布时间】:2016-10-21 20:14:27 【问题描述】:

在使用可变参数模板时,遵循this SO question(注意:不是必须去那里回答这个问题),对于以下重载的模板,我遇到了 clang (3.8) 和 g++ (6.1) 的不同行为功能:

template <class... Ts>
struct pack  ;

template <class a, class b>
constexpr bool starts_with(a, b) 
    return false;


template <template <typename...> class PACK_A,
          template <typename...> class PACK_B, typename... Ts1, typename... Ts2>
constexpr bool starts_with(PACK_A<Ts1..., Ts2...>, PACK_B<Ts1...>) 
    return true;


int main() 
   std::cout << std::boolalpha;
   std::cout << starts_with(pack<int, float, double>(),
                            pack<float, int, double>())        << std::endl;
   std::cout << starts_with(pack<int, float, double>(),
                            pack<int, float, double, int>())   << std::endl;
   std::cout << starts_with(pack<int, float, double>(),
                            pack<int, float, int>())           << std::endl;
   std::cout << starts_with(pack<int, float, double>(),
                            pack<int, float, double>())        << std::endl;
   std::cout << starts_with(pack<int, float, double>(),
                            pack<int>())                       << std::endl;

代码:http://coliru.stacked-crooked.com/a/b62fa93ea88fa25b

输出

|---|-----------------------------------------------------------------------------|
| # |starts_with(a, b)                  | expected    | clang (3.8) | g++ (6.1)   |
|---|-----------------------------------|-------------|-------------|-------------|
| 1 |a: pack<int, float, double>()      |  false      |  false      |  false      |
|   |b: pack<float, int, double>()      |             |             |             |
|---|-----------------------------------|-------------|-------------|--------- ---|
| 2 |a: pack<int, float, double>()      |  false      |  false      |  false      |
|   |b: pack<int, float, double, int>() |             |             |             |
|---|-----------------------------------|-------------|-------------|--------- ---|
| 3 |a: pack<int, float, double>()      |  false      |  false      |  false      |
|   |b: pack<int, float, int>()         |             |             |             |
|---|-----------------------------------|-------------|-------------|--------- ---|
| 4 |a: pack<int, float, double>()      |  true       |  true       |  false      |
|   |b: pack<int, float, double>()      |             |             |             |
|---|-----------------------------------|-------------|-------------|--------- ---|
| 5 |a: pack<int, float, double>()      |  true       |  false      |  false      |
|   |b: pack<int>()                     |             |             |             |
|---|-----------------------------------------------------------------------------|

最后两种情况(4 和 5)存在问题:我对 更专业的模板 的期望是否错误?如果是这样,在情况 4、clang 或 g++ 中谁是正确的? (请注意,代码编译时没有任何错误或警告,但结果不同)。

为了自己回答这个问题,我多次检查了规范 (14.5.6.2 Partial ordering of function templates) 和 cppreference 中的“更专业”的规则——似乎更专业的规则会给出我期望的结果(一个如果不是,可能会出现歧义错误,但也不是这种情况)。那么,我在这里缺少什么?


等等(1):请不要急着把赫伯·萨特的“prefer not to overload templates”和他的template methods quiz带来。这些当然很重要,但是该语言仍然允许模板重载! (这确实是你不应该重载模板的一个重点——在某些极端情况下,它可能会混淆两个不同的编译器,或者混淆程序员。但问题不在于是否使用它,它是:如果你使用它,正确的行为是什么?)。

等等(2):请不要急于带来其他可能的解决方案。肯定有的。这里有两个:one with inner struct 和 another with inner static methods。两者都是合适的解决方案,都按预期工作,但关于上述模板重载行为的问题仍然存在。

【问题讨论】:

我怀疑结果是由于PACK_A&lt;Ts1..., Ts2...&gt;。编译器应该无法推断出第一个参数包Ts1...,因为它不在模板的末尾。因此它应该是空的,因此永远不应该选择重载。我不知道clang怎么会在案例4中选择它。 作为@W.F.说,标准告诉我们 “如果 P 的模板参数列表包含不是最后一个模板参数的包扩展,则整个模板参数列表是非推导上下文。” (14.8. 2.5/8),所以 g++ 似乎是对的,也是错的。 确认,将签名更改为template &lt;typename... Ts1, typename... Ts2 &gt; constexpr bool starts_with(pack&lt;Ts1..., Ts2...&gt;, pack&lt;Ts1...&gt;)在gcc中修复它。 @AmirKirsh Ts... 从你的例子中没有推断出来。它们在模板化结构被专门化时是已知的。这是这里最关键的区别。由于Ts... 不是在那里推导出来的,它可以按原样放置在那里。 什么?您应该重载函数模板。你不应该专门化他们。 【参考方案1】:

正如霍尔特所说,在可变参数模板参数推导方面,标准非常严格:

14.8.2.5/9

如果 P 有一个包含 T 或 i 的形式,那么每个参数 Pi 将相应的模板参数列表 P 与相应的 A 的对应模板实参列表的实参 Ai。如果 P 的模板参数列表包含一个不是 最后一个模板参数,整个模板参数列表是一个 非推导上下文。 如果 Pi 是包展开,那么 Pi 的模式 与模板参数列表中的每个剩余参数进行比较 A. 每次比较都推导出模板参数以供后续使用 由 Pi 扩展的模板参数包中的位置。

这是由 T.C. 解释的。这意味着Ts1... 可以从第二个参数推导出来,但它没有给Ts2... 推导留下空间。因此,显然clang在这里是正确的,而gcc是错误的......只有当第二个参数包含完全相同的模板参数时,才应该选择重载,例如:

starts_with(pack<int, float, double>(), pack<int, float, double>())

仍然是示例 5。不满足此要求并且不允许编译器选择重载。

【讨论】:

PACK_A&lt;Ts1..., Ts2...&gt; 是非推断上下文,但这并不意味着Ts1... 被推断为空。这意味着它不是从第一个参数推导出来的,但它可以从第二个参数推导出来。 Ts2...,OTOH,根本不推演,因此推演为空。 @T.C.所以starts_with(pack&lt;int, float, double&gt;(), pack&lt;int, float, double&gt;()) -> true 实际上是预期的行为? 有点。它打破了eel.is/c++draft/temp.param#11,但没有人真正诊断出来。 @T.C.好吧,我想我在规范中找到了规则... 14.8.2.1/1:“当函数参数包出现在非推导上下文中时...,永远不会推导该参数包的类型。”但是,在初始场景中,clang 是错误的,而 g++ 是正确的,而在 Richard Hodges 场景中两者都是错误的。非推导上下文将导致该模板 (SFINAE) 失败,并导致“错误”结果。不是吗? Ts1 是模板参数包,不是函数参数包。一个函数参数包是template&lt;class... Ts&gt; void f(Ts... p);中的p【参考方案2】:

仅供参考:不是答案。这是对cmets中一个问题的回应:

在 gcc5.3 上进行以下小的更改会导致它产生预期的结果,或者至少与 clang 的结果相同。

rhodges@dingbat:~$ cat nod.cpp
#include <iostream>

using namespace std;

template <class... Ts>
struct pack  ;

template <class a, class b>
constexpr bool starts_with(a, b) 
    return false;


template <typename... Ts1, typename... Ts2 >
constexpr bool starts_with(pack<Ts1..., Ts2...>, pack<Ts1...>) 
    return true;


int main() 
   std::cout << std::boolalpha;
   std::cout << starts_with(pack<int, float, double>(), pack<float, int, double>()) << std::endl;
   std::cout << starts_with(pack<int, float, double>(), pack<int, float, double, int>()) << std::endl;
   std::cout << starts_with(pack<int, float, double>(), pack<int, float, int>()) << std::endl;
   std::cout << starts_with(pack<int, float, double>(), pack<int, float, double>()) << std::endl;
   std::cout << starts_with(pack<int, float, double>(), pack<int>()) << std::endl;



rhodges@dingbat:~$ g++ -std=c++14 nod.cpp && ./a.out
false
false
false
true
false
rhodges@dingbat:~$ g++ --version
g++ (Ubuntu 5.3.1-14ubuntu2.1) 5.3.1 20160413
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

rhodges@dingbat:~$

为了记录,修改程序以在推断的上下文中评估所有包会在两个平台上取得成功:

rhodges@dingbat:~$ cat nod.cpp
#include <iostream>

using namespace std;

template <class... Ts>
struct pack  ;

template <class a, class b>
constexpr bool starts_with_impl(a, b) 
    return false;


template<typename...LRest>
constexpr bool starts_with_impl(pack<LRest...>, pack<>)

    return true;


template<typename First, typename...LRest, typename...RRest>
constexpr bool starts_with_impl(pack<First, LRest...>, pack<First, RRest...>)

    return starts_with_impl(pack<LRest...>(), pack<RRest...>());


template <typename... Ts1, typename... Ts2 >
constexpr bool starts_with(pack<Ts2...> p1, pack<Ts1...> p2) 
    return starts_with_impl(p1, p2);


int main() 
    std::cout << std::boolalpha;
    std::cout << starts_with(pack<int, float, double>(), pack<float, int, double>()) << std::endl;
    std::cout << starts_with(pack<int, float, double>(), pack<int, float, double, int>()) << std::endl;
    std::cout << starts_with(pack<int, float, double>(), pack<int, float, int>()) << std::endl;
    std::cout << starts_with(pack<int, float, double>(), pack<int, float, double>()) << std::endl;
    std::cout << starts_with(pack<int, float, double>(), pack<int>()) << std::endl;



rhodges@dingbat:~$ g++ -std=c++14 nod.cpp && ./a.out
false
false
false
true
true

归功于 W.F.引导我朝这个方向前进。

【讨论】:

以上是关于哪个是更专业的模板功能? clang 和 g++ 对此有所不同的主要内容,如果未能解决你的问题,请参考以下文章

Clang 不允许 static_cast 到带有模板的父类,而 g++ 和 icc 允许

g++ 和 clang++ 使用变量模板和 SFINAE 的不同行为

为啥接受数组的 C++ 模板并不比接受 GCC 5.3 和 Clang 4.0 的指针更专业?

Clang 可变参数模板专业化错误:不可演绎的模板参数

解决 VC++12 中的模板专业化错误?

带有非类型参数的奇怪模板实例化错误