整数非类型参数和非整数非类型的模板偏特化,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
.
的最新版本
这里有一个警告。当您将 -std
从 c++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.1
和 gcc 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 的代码中添加以下行(以及我示例中的 bar
、b1
定义):
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 实现保持不变。
下面是一个简单的模板偏特化:
// #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
.
这里有一个警告。当您将 -std
从 c++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.1
和 gcc 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 的代码中添加以下行(以及我示例中的 bar
、b1
定义):
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 实现保持不变。