在定义更受约束的版本之前和之后调用函数模板会产生奇怪的结果
我的同事今天向我展示了以下示例:
Run on gcc.godbolt.org
#include <concepts>
#include <iostream>
template <typename T>
void foo(T)
{
std::cout << "1n";
}
template <typename T>
void bar(T value)
{
foo(value);
}
void foo(std::same_as<int> auto)
{
std::cout << "2n";
}
在这里,bar(42);并分别foo(42);打印1和2,如果您只调用其中之一。
如果您同时调用两者(按此顺序),则:
- 叮当印
11。 - GCC 发出链接器错误,抱怨
foo. - MSVC
22在发布版本中打印并在调试版本中发出类似的链接器错误(可能受增量链接的影响,未调查)。
这里发生了什么?代码在我看来格式良好,但也许我错了?
回答
凉爽的。每个编译器都是错误的。
在 内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)应该调用foo(same_as<int> auto)应该打印 2的约束。
总结一下:
-
Clang 犯了这个错误,因为它显然缓存了
foo<int>调用,这是不正确的 - 另一个foo重载不是专业化,它是一个重载,需要单独考虑。 -
gcc 是正确的,因为两个不同的
foo调用调用了两个不同的foo函数模板,但错误的是它破坏了相同的内容,因此我们最终会出现链接器错误。这是安腾 ABI #24。 -
MSVC 在
barfor 中的依赖于参数的查找中得到了这个错误,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';
}
然后铛编译(即它正确地给你bar(42) == 1,并foo(42) == 2从该点),但然后打印2反正两次。
虽然 gcc 仍然可以编译,但只是有相同的链接器错误,因为它对两个函数模板的处理相同。