整数非类型参数和非整数非类型的模板偏特化,g++ 和 clang 的区别

Template partial specialization for integral non-type parameters and non-integral non-types, difference between g++ and clang

下面是一个简单的模板偏特化:

// #1
template <typename T, T n1, T n2>
struct foo { 
    static const char* scenario() {
        return "#1 the base template";
    }
};

// #2
// partial specialization where T is unknown and n1 == n2
template <typename T, T a>
struct foo<T, a, a> { 
    static const char* scenario() {
        return "#2 partial specialization";
    }
};

下面的主要内容在 g++ (6.1)clang++ (3.8.0) 上得到了不同的结果:

extern const char HELLO[] = "hello";
double d = 2.3;

int main() {
    cout <<   foo<int, 1, 2>                    ::scenario() << endl;                   
    cout <<   foo<int, 2, 2>                    ::scenario() << endl;                   
    cout <<   foo<long, 3, 3>                   ::scenario() << endl;                  
    cout <<   foo<double&, d, d>                ::scenario() << endl;               
    cout <<   foo<double*, &d, &d>              ::scenario() << endl;             
    cout <<   foo<double*, nullptr, nullptr>    ::scenario() << endl;   
    cout <<   foo<int*, nullptr, nullptr>       ::scenario() << endl;      
    cout <<   foo<nullptr_t, nullptr, nullptr>  ::scenario() << endl; 
    cout <<   foo<const char*, HELLO, HELLO>    ::scenario() << endl;
}

g++clang++

的结果

<b>#</b> | <b>代码</b> | <b>g++ (6.1)</b> | <b>clang++ (3.8.0)</b> |
1 | foo<int, 1, 2> | #1 as expected | #1 as expected |
2 | foo<int, 2, 2> | #2 as expected | #2 as expected |
3 | foo<long, 3, 3> | #2 as expected | #2 as expected |
4 | foo<double&, d, d> | #1 -- why? | #2 as expected |
5 | foo<double*, &d, &d> | #2 as expected | #2 as expected |
6 | foo<double*, nullptr, nullptr> | #2 as expected | #1 -- why? |
7 | foo<int*, nullptr, nullptr> | #2 as expected | #1 -- why? |
8 | foo<nullptr_t, nullptr, nullptr> | #2 as expected | #1 -- why? |
9 | foo<const char*, HELLO, HELLO> | #2 as expected | #2 as expected |

哪一个是正确的?

代码:https://godbolt.org/z/4GfYqxKn3


编辑,2021 年 12 月:

自最初 post 以来的这些年来,结果发生了变化,and were even identical for gcc and clang at a certain point in time, but checking again, g++ (11.2) and clang++ (12.0.1) changed their results on references (case 4), but still differ on it。似乎目前 gcc 一切正常,而 clang 在参考案例中是错误的。

<b>#</b> | <b>代码</b> | <b>g++ (11.2)</b> | <b>clang++ (12.0.1)</b> |
1 | foo<int, 1, 2> | #1 as expected | #1 as expected |
2 | foo<int, 2, 2> | #2 as expected | #2 as expected |
3 | foo<long, 3, 3> | #2 as expected | #2 as expected |
4 | foo<double&, d, d> | #2 as expected | #1 -- why? |
5 | foo<double*, &d, &d> | #2 as expected | #2 as expected |
6 | foo<double*, nullptr, nullptr> | #2 as expected | #2 as expected |
7 | foo<int*, nullptr, nullptr> | #2 as expected | #2 as expected |
8 | foo<nullptr_t, nullptr, nullptr> | #2 as expected | #2 as expected |
9 | foo<const char*, HELLO, HELLO> | #2 as expected | #2 as expected |

#4 格式错误,我很惊讶它能编译。一方面,double 不能用作非类型模板参数。另一方面,class 模板使用与偏序函数模板相同的规则。该标准提供了生成以执行排序的 "imaginary" 函数的示例:

template<int I, int J, class T> class X { };
template<int I, int J> class X<I, J, int> { }; // #1
template<int I>        class X<I, I, int> { }; // #2
template<int I, int J> void f(X<I, J, int>); // A
template<int I>        void f(X<I, I, int>); // B

对于您的示例,它看起来像这样:

template <typename T, T n1, T n2>
struct foo { 
};

template <typename T, T n1, T n2>
void bar(foo<T, n1, n2>)
{
    std::cout << "a";
}

template <typename T, T a>
void bar(foo<T, a, a>)
{
    std::cout << "b";
}

模板参数推导用于确定哪个函数比另一个函数更专业。 double& 应该推导为 double,因此两个特化应该相等,也就是 bar(foo<double&, d, d>{}); 不明确。瞧瞧 GCC 和 Clang 抱怨:

GCC error

main.cpp:14:6: note: template argument deduction/substitution failed: main.cpp:26:29: note: mismatched types 'double&' and 'double'

 bar(foo<double&, d, d>{});
                         ^

Clang error

note: candidate template ignored: substitution failure [with T = double &]: deduced non-type template argument does not have the same type as the its corresponding template parameter ('double' vs 'double &')

再一次,如果您删除引用,他们都会正确地抱怨使用 double 作为非类型模板参数。

我不会测试其余部分,但您可能会发现类似的结果。

让我们从根据 C++ 标准确定什么是正确的开始,并将其与实际编译器进行比较。我很确定,它应该像您期望的那样工作,即 #1 用于第一种情况,#2 用于所有其他情况(尽管有一些注意事项适用,请参阅后面)。

正如 evzh 在他的评论中指出的那样(2020 年 7 月 1 日在 3:30),gcc 7.2 和 clang 4.0.0 是最早可以正常工作的版本。它们一直正常工作到 -std=c++11.

的最新版本

这里有一个警告。当您将 -stdc++14 增加到 c++17 时,Clang 的工作方式有所不同。在 EDIT,2021 年 12 月 中的问题中,您使用的是 -std=c++20,它在 c++17 之后。这就是您遇到 why? 案例的原因。

我的假设是编译器在 2016 年存在错误,它们与上述确定的正确行为不同,并且 -std=c++17 或更高版本的 clang 12.0.1 在 foo<double&, d, d> 的情况下仍然存在错误.

Clang 是一个非常迂腐的编译器,所以我可能是错的。有一点机会,标准中的某些内容发生了变化,这使得 clang 正确而其他编译器错误。这值得在单独的 question.

中询问

这里有一个简化的测试来演示您的所有情况:demo。我添加了四个编译器来展示 foo<double&, d, d> 的行为方式。

我使用了最新的编译器并试图解释 foo 行为者。

foo函数声明如下

// #1
template <typename T, T n1, T n2>
struct foo { 
    static const char* scenario() {
        return "#1 the base template";
    }
};

// #2
// partial specialization where T is unknown and n1 == n2
template <typename T, T a>
struct foo<T, a, a> { 
    static const char* scenario() {
        return "#2 partial specialization";
    }
};

这里的问题是调用立即将 T 类型推导为 double 而不是预期的 double&,然后将其隐式转换为 double &。 这就是为什么不同的编译器以不同的方式运行的原因。 将 foo 函数更改为以下将解决问题。

// #1
template <typename T, typename std::type_identity<T>::type n1, typename std::type_identity<T>::type n2>
struct foo { 
  static const char* scenario() {return "#1 the basic: template <typename T, T n1, T n2>";}
};

// #2
// partial specialization where T is unknown and n1 == n2
template <typename T, typename std::type_identity<T>::type a>
struct foo<T, a, a> { 
  static const char* scenario() {return "#2 partial specialization: template<typename T, T a> foo<typename T, a, a>";}
};

我将把我的回答献给案例 #4,因为根据 OP 的编辑,编译器现在同意案例 #6-8:

# | The code | g++ (6.1) | clang++ (3.8.0) |

4 | foo<double&, d, d> | #1 -- why? | #2 as expected |

似乎 clang++ 3.8.0 的行为是正确的,而 gcc 6.1 拒绝对这种情况进行完美的局部特化,因为 gcc 7.2 中修复了以下错误:

Bug 77435 - Dependent reference non-type template parameter not matched for partial specialization

编译器代码中有一个 diff 关键更改:

// Was: else if (same_type_p (TREE_TYPE (arg), tparm))
else if (same_type_p (non_reference (TREE_TYPE (arg)), non_reference(tparm)))

gcc 7.2 之前,当依赖类型 T& 与参数 T 类型的参量匹配时,编译器会错误地拒绝它。这种行为可以在一个更清晰的例子中得到证明:

template <typename T, T... x>
struct foo { 
    static void scenario() { cout << "#1" << endl; }
};

// Partial specialization when sizeof...(x) == 1
template <typename T, T a>
struct foo<T, a> { 
    static void scenario() { cout << "#2" << endl; }
};

T = const int 的情况下,gcc 6.1gcc 7.2 的行为是相同的:

const int i1 = 1, i2 = 2;
foo<const int, i1, i2>::scenario(); // Both print #1
foo<const int, i1>::scenario();     // Both print #2

但是在 T = const int& 的情况下 gcc 6.1 的行为是拒绝正确的偏特化并选择基本实现:

foo<const int&, i1, i2>::scenario(); // Both print #1
foo<const int&, i1>::scenario();     // gcc 6.1 prints #1 but gcc 7.2 prints #2

它影响任何引用类型,这里有更多示例:

double d1 = 2.3, d2 = 4.6;

struct bar {};
bar b1, b2;

foo<double&, d1, d2>::scenario(); // Both print #1
foo<double&, d1>::scenario();     // gcc 6.1 prints #1 but gcc 7.2 prints #2
foo<bar&, b1, b2>::scenario();    // Both print #1
foo<bar&, b1>::scenario();        // gcc 6.1 prints #1 but gcc 7.2 prints #2

您可以在此处 运行 这个示例:https://godbolt.org/z/Y1KjazrMP

gcc 似乎在 gcc 7.1 之前犯了这个错误,但是从 gcc 7.2 到当前版本,由于上面的错误修复,它正确地选择了部分专业化。

总而言之,案例 #4 的结果只是一个更普遍问题的症状,它的发生只是因为 double& 是引用类型。为了证明这一说法,请尝试在 OP 的代码中添加以下行(以及我示例中的 barb1 定义):

cout << foo<bar&, b1, b1>::scenario() << endl;

并观察 gcc 6.1 再次打印 "#1 the base template"gcc 7.2 并继续按预期打印 "#2 partial specialization"


编辑

关于 OP 编辑​​中的 follow-up 问题:

# | The code | g++ (11.2) | clang++ (12.0.1) |

4 | foo<double&, d, d> | #2 as expected | #1 -- why? |

我认为 g++ (11.2) 是正确的。

请注意 clang 并没有完全翻转它的答案,因为在你的 link 中,你使用了 c++20 标准,但如果你将其改回 c++14与原始问题一样,即使 clang++ 12.0.1 也同意 g++ 11.2 并选择部分专业化。

实际上,c++17 中的 clang 也会发生这种情况,这似乎是 clang 中的一个问题,从这个标准开始,直到今天才得到解决。

如果您尝试将以下测试用例添加到您的代码中:

TEST (foo<const int, 2, 2>); // clang (c++17/20) prints #1 and gcc (any) prints #2

clang 也选择基本模板而不是像 gcc 那样的偏特化,而在此测试用例中:

TEST (foo<int, 2, 2>); // Both agree on #2

他们都同意,我觉得这很奇怪,因为这向类型添加了 const 不应该影响部分专业化的适用性而且似乎 clang 不会这样做仅供参考,但对于常量也是如此!并且仅当标准 >= C++17.

顺便说一句,这个问题也可以在我的示例中重现: https://godbolt.org/z/W9q83j3Pq

观察到 clang 8.0.0 仅通过更改语言标准就与自身不一致,并且即使在不需要参数值相等的这种简单情况下,它也会一直这样做直到 clang 13.0.0

clang 中这些奇怪的模板推导引发了足够多的“危险信号”,所以我必须得出结论 g++ (11.2) 是正确的。

我的大胆猜测是 - C++17 引入了 CTAD,这使得 clang 的行为与 class 模板推导不同,这个问题在某种程度上与其新实现有关,而旧的 C++14 实现保持不变。