哪个是更专业的模板函数? clang 和 g++ 的不同之处在于

Which is the more specialized template function? clang and g++ differ on that

在使用可变参数模板时,遵循 (注意:跟随这个问题并不是必须去那里),我发现了 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) and in cppreference 中的 "more specialized" 规则——似乎更专业的规则会给出我期望的结果(一个如果不是,可能会出现歧义错误,但情况也不是这样)。那么,我在这里缺少什么?


等等(1):请不要着急带上“prefer not to overload templates" of Herb Sutter and his template methods quiz。这些固然重要,但语言仍然允许模板重载!(这确实是一个加强点,为什么你不喜欢重载模板——在某些边缘情况下,它可能会混淆两个不同的编译器,或者混淆程序员。但问题不在于是否使用它,而是:什么是正确的如果你确实使用它会有什么行为?).

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

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

在 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。指导我朝这个方向前进。

正如 Holt 提到的,在可变模板参数推导方面标准非常严格:

14.8.2.5/9

If P has a form that contains T or i, then each argument Pi of the respective template argument list P is compared with the corresponding argument Ai of the corresponding template argument list of A. If the template argument list of P contains a pack expansion that is not the last template argument, the entire template argument list is a non-deduced context. If Pi is a pack expansion, then the pattern of Pi is compared with each remaining argument in the template argument list of A. Each comparison deduces template arguments for subsequent positions in the template parameter packs expanded by Pi.

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

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

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