在定义更受约束的版本之前和之后调用函数模板会产生奇怪的结果

Calling a function template before and after a more constrained version is defined gives weird results

我的同事今天向我展示了以下示例:

Run on gcc.godbolt.org

#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);分别打印12如果你只调用其中一个。

如果您同时调用 (按此顺序),则:

这是怎么回事?代码对我来说看起来格式正确,但也许我错了?

酷。每个编译器都是错误的。

bar 中,对 foo(value) 的调用只有不受约束的 foo<T> 在范围内可见。因此,当我们调用 foo(value) 时,唯一可能的候选者是 (1) 那个 (2) 任何依赖于参数的查找找到的。由于我们示例中的 T=intint 没有关联的名称空间,因此 (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) == 1foo(42) == 2)但是然后打印 2 两次。

虽然 gcc 仍然编译,只是有相同的链接器错误,因为它对两个函数模板的破坏相同。