哪个是更专业的模板函数?clang 和 g++ 在这方面有所不同。

19

在尝试使用可变参数模板时,参考了这个Stack Overflow的问题(注意:不必去那里查看问题以回答本问题),我发现以下模板重载函数在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):请不要急于使用Herb Sutter的“prefer not to overload templates”以及他的template methods quiz。这些确实很重要,但语言仍然允许模板重载!(这确实是为什么你应该尽量不使用模板重载的一个加强点——在某些边缘情况下,它可能会混淆两个不同的编译器,或者混淆程序员。但问题不在于是否使用它,而在于:如果你使用它,正确的行为是什么?)。

等待(2):请不要急于提出其他可能的解决方案。肯定有其他解决方案。以下是其中两种:带有内部结构体的一种另一种带有内部静态方法的解决方案。两种都是适当的解决方案,都能按预期工作,但上述模板重载行为的问题仍然存在。


1
我怀疑结果是由于 PACK_A<Ts1..., Ts2...>。编译器不应该能够推断出第一个参数包 Ts1...,因为它不在模板的末尾。因此,它应该为空,因此这个重载应该永远不会被选择。我不知道为什么 clang 在第四种情况下选择了它。 - W.F.
2
正如@W.F.所说,标准告诉我们:“如果P的模板参数列表包含不是最后一个模板参数的展开包,则整个模板参数列表是无法推导的上下文。”(14.8.2.5/8),因此g++似乎是正确的,而clang是错误的。 - Holt
1
确认一下,在gcc中将签名更改为template <typename... Ts1, typename... Ts2> constexpr bool starts_with(pack<Ts1..., Ts2...>, pack<Ts1...>)可以解决这个问题。 - Richard Hodges
1
@AmirKirsh,你示例中的Ts...并没有被推导出来,而是在模板化结构体被特化时就已知。这是最关键的区别。由于Ts...在那里没有被推导,因此它可以被放置在那里,就像原样一样。 - W.F.
2
什么?你应该重载函数模板。你不应该专门化它们。 - n. m.
显示剩余5条评论
2个回答

4
正如Holt所提到的,当涉及可变模板参数推导时,标准非常严格:
14.8.2.5/9 如果P具有包含T或i的形式,则将P的每个参数Pi与A的相应模板参数列表的相应参数Ai进行比较。如果P的模板参数列表包含不是最后一个模板参数的展开包,则整个模板参数列表是无法推导的上下文。如果Pi是一个展开包,则将Pi的模式与A的模板参数列表中的每个剩余参数进行比较。每次比较都会推导出后续位置的模板参数,这些参数由Pi扩展。
根据T.C.的解释,这意味着可以从第二个参数中推导出来,但是没有为留下推导的空间。因此,显然clang是正确的,gcc是错误的...只有当第二个参数包含完全相同的模板参数时,才应选择重载。
starts_with(pack<int, float, double>(), pack<int, float, double>())

然而,示例5并不符合此要求,也不允许编译器选择重载。


1
PACK_A<Ts1..., Ts2...> 是一个无法推断的上下文,但这并不意味着 Ts1... 被推断为空。它意味着它不是从第一个参数中推断出来的,但可以从第二个参数中推断出来。另一方面,Ts2... 则根本没有被推断,因此被推断为空。 - T.C.
@T.C. 所以 starts_with(pack<int, float, double>(), pack<int, float, double>()) -> true 是预期的行为吗? - W.F.
1
有点儿。它违反了http://eel.is/c++draft/temp.param#11,但实际上没有人诊断出来。 - T.C.
@T.C. 14.8.2.5/9将PACK_A视为“整个模板参数列表是不可推导的上下文”,这很公平。但是,另一个可推导的上下文可以推导出非推导上下文。因此,让我们遵循14.1/11:“函数模板的模板参数包不得后跟另一个模板参数除非该模板参数可以从函数的参数类型列表中推导出...”--我相信我们处于“参数可以从参数类型列表中推导出”的情况(PACK_B)--这意味着情况4和5可以被推导并应按预期工作(true,true)! - Amir Kirsh
1
@T.C. 嗯,我想我在规范中找到了这个规则... 14.8.2.1/1: "当函数参数包出现在无法推导的上下文中时...,该参数包的类型永远不会被推导。" 但是,然后,clang在最初的情况下是错误的,g++是正确的,而在Richard Hodges的情况下两者都是错误的。一个未推导的上下文应该使这个模板失败(SFINAE),从而产生“false”的结果。不是吗? - Amir Kirsh
1
Ts1 是一个模板参数包,而不是函数参数包。函数参数包是 template<class... Ts> void f(Ts... p); 中的 p - T.C.

2

仅供参考:不是答案。这是对评论中问题的回答:

对于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.在这方面的指导。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接