原子函数指针调用在 gcc 中编译,但在 clang 和 msvc 中不编译

Atomic function pointer call compiles in gcc, but not in clang and msvc

从原子函数指针调用函数时,例如:

#include <atomic>
#include <type_traits>

int func0(){ return 0; }

using func_type = std::add_pointer<int()>::type;

std::atomic<func_type> f = { func0 };

int main(){
        f();
}

gcc 一点也不抱怨,而 clang 和 msvc 调用有问题 f():

Clang 还指定可能的调用候选者为:

这种波动性差异似乎让 clang 和 msvc 感到困惑,但对 gcc 却没有。

当调用从 f() 更改为 f.load()() 时,代码在上述所有编译器中均有效。这更令人困惑,因为据说 load() and operator T() 都有 constconst volatile 重载——如果隐式转换不起作用,我希望 load() 不会工作也一样。隐式转换(相对于成员调用)中的规则是否有所不同?

那么,gcc 接受该代码是错误的吗? clang 和 msvc 是不是错误的报错了?还有其他错误或正确的组合吗?


这主要是一个理论问题,但如果有更好的方法来获得原子函数指针,我很想知道。

Clang 和 MSVC 是正确的。

对于 class 的函数指针的每个转换函数,一个 so-called 代理调用函数 添加到重载决议中,如果选择的话首先通过此运算符重载将对象转换为函数指针,然后通过函数指针调用该函数。 [over.call.object]/2.

中对此进行了解释

但是,代理调用函数不会以任何方式翻译转换运算符的cv-qualifiers。因此,由于 std::atomic 有一个转换运算符 volatile 和一个不是,因此将有两个无法区分的代理调用函数。这些也是唯一的候选者,因为 std::atomic 没有任何实际的 operator(),因此重载解析必须始终是模棱两可的。

标准中甚至有一个脚注提到可能会发生这种情况,请参阅 [over.call.object]/footnote.120

通过直接调用 .load()volatile-限定符将成为重载解析中的 tie-breaker,因此不会出现此问题。

With (*f)() 以函数指针类型作为参数对 (built-in) operator* 执行重载决议。通过两个转换函数有两个隐式转换序列。标准对此不是很清楚,但我认为其意图是这不会导致不明确的转换序列(这也意味着在选择时不明确的重载解析)。相反,我认为通过转换函数进行初始化的规则仅应用于 select 转换之一,这将使它明确地成为 volatile 合格的转换。