在定义更受约束的版本之前和之后调用函数模板会产生奇怪的结果
Calling a function template before and after a more constrained version is defined gives weird results
我的同事今天向我展示了以下示例:
#include <concepts>
#include <iostream>
template <typename T>
void foo(T)
{
std::cout << "1\n";
}
template <typename T>
void bar(T value)
{
foo(value);
}
void foo(std::same_as<int> auto)
{
std::cout << "2\n";
}
这里,bar(42);
和foo(42);
分别打印1
和2
,如果你只调用其中一个。
如果您同时调用 (按此顺序),则:
- Clang 打印
1
1
.
- GCC 发出链接器错误,抱怨
foo
. 的多个定义
- MSVC 在发布版本中打印
2
2
并在调试版本中发出类似的链接器错误(可能受增量链接影响,未调查)。
这是怎么回事?代码对我来说看起来格式正确,但也许我错了?
酷。每个编译器都是错误的。
在 bar
中,对 foo(value)
的调用只有不受约束的 foo<T>
在范围内可见。因此,当我们调用 foo(value)
时,唯一可能的候选者是 (1) 那个 (2) 任何依赖于参数的查找找到的。由于我们示例中的 T=int
和 int
没有关联的名称空间,因此 (2) 是一个空集。结果,当 bar(42)
调用 foo(42)
时,即 foo
是不受约束的模板,它应该打印 1.
另一方面,在 main
中,foo(42)
有两种不同的重载需要考虑:受约束的和不受约束的。受约束的是可行的,并且比不受约束的更受约束,所以这是首选。因此,从 main()
中的 ,foo(42)
应该调用应打印 2.
的约束 foo(same_as<int> auto)
总结一下:
Clang 弄错了,因为它显然缓存了 foo<int>
调用,这是不正确的 - 另一个 foo
重载不是特化,它是一个重载,它需要另行考虑。
gcc 是正确的,因为两个不同的 foo
调用调用了两个不同的 foo
函数模板,但错误的是它对两者进行了相同的处理,因此我们最终出现链接器错误。这是 Itanium ABI #24.
MSVC 在 bar
中对 foo(value)
的参数相关查找发现了后来声明的 foo
.
更有趣的是,如果您将函数更改为 constexpr int
而不是 void
,这样您就可以在编译时验证此行为...如:
#include <concepts>
#include <iostream>
template <typename T>
constexpr int foo(T)
{
return 1;
}
template <typename T>
constexpr int bar(T value)
{
return foo(value);
}
constexpr int foo(std::same_as<int> auto)
{
return 2;
}
static_assert(bar(42) == 1);
static_assert(foo(42) == 2);
int main()
{
std::cout << bar(42) << '\n';
std::cout << foo(42) << '\n';
}
然后 clang 编译(即它确实从那个地方正确地给你 bar(42) == 1
和 foo(42) == 2
)但是然后打印 2
两次。
虽然 gcc 仍然编译,只是有相同的链接器错误,因为它对两个函数模板的破坏相同。
我的同事今天向我展示了以下示例:
#include <concepts>
#include <iostream>
template <typename T>
void foo(T)
{
std::cout << "1\n";
}
template <typename T>
void bar(T value)
{
foo(value);
}
void foo(std::same_as<int> auto)
{
std::cout << "2\n";
}
这里,bar(42);
和foo(42);
分别打印1
和2
,如果你只调用其中一个。
如果您同时调用 (按此顺序),则:
- Clang 打印
1
1
. - GCC 发出链接器错误,抱怨
foo
. 的多个定义
- MSVC 在发布版本中打印
2
2
并在调试版本中发出类似的链接器错误(可能受增量链接影响,未调查)。
这是怎么回事?代码对我来说看起来格式正确,但也许我错了?
酷。每个编译器都是错误的。
在 bar
中,对 foo(value)
的调用只有不受约束的 foo<T>
在范围内可见。因此,当我们调用 foo(value)
时,唯一可能的候选者是 (1) 那个 (2) 任何依赖于参数的查找找到的。由于我们示例中的 T=int
和 int
没有关联的名称空间,因此 (2) 是一个空集。结果,当 bar(42)
调用 foo(42)
时,即 foo
是不受约束的模板,它应该打印 1.
另一方面,在 main
中,foo(42)
有两种不同的重载需要考虑:受约束的和不受约束的。受约束的是可行的,并且比不受约束的更受约束,所以这是首选。因此,从 main()
中的 ,foo(42)
应该调用应打印 2.
foo(same_as<int> auto)
总结一下:
Clang 弄错了,因为它显然缓存了
foo<int>
调用,这是不正确的 - 另一个foo
重载不是特化,它是一个重载,它需要另行考虑。gcc 是正确的,因为两个不同的
foo
调用调用了两个不同的foo
函数模板,但错误的是它对两者进行了相同的处理,因此我们最终出现链接器错误。这是 Itanium ABI #24.MSVC 在
bar
中对foo(value)
的参数相关查找发现了后来声明的foo
.
更有趣的是,如果您将函数更改为 constexpr int
而不是 void
,这样您就可以在编译时验证此行为...如:
#include <concepts>
#include <iostream>
template <typename T>
constexpr int foo(T)
{
return 1;
}
template <typename T>
constexpr int bar(T value)
{
return foo(value);
}
constexpr int foo(std::same_as<int> auto)
{
return 2;
}
static_assert(bar(42) == 1);
static_assert(foo(42) == 2);
int main()
{
std::cout << bar(42) << '\n';
std::cout << foo(42) << '\n';
}
然后 clang 编译(即它确实从那个地方正确地给你 bar(42) == 1
和 foo(42) == 2
)但是然后打印 2
两次。
虽然 gcc 仍然编译,只是有相同的链接器错误,因为它对两个函数模板的破坏相同。