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

ja2*_*142 18 c++ atomic language-lawyer stdatomic

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

#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();
}
Run Code Online (Sandbox Code Playgroud)

gcc 根本不抱怨,而 clang 和 msvc 在调用方面有问题f()

  • [clang]:错误:对“std::atomic<func_type>”类型的对象的调用(又名“atomic<int (*)()>”)不明确
  • [msvc]:有不止一种方法可以为参数列表调用“std::atomic<func_type>”类型的对象

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

  • operator __pointer_type() const noexcept
  • operator __pointer_type() const volatile noexcept

看起来这种波动性的差异对于 clang 和 msvc 来说是令人困惑的,但对于 gcc 却不是。

f()当 call从改为 时f.load()(),代码可以在所有上述编译器中运行。这更令人困惑,因为据说load()和都有和重载 - 如果隐式转换不起作用,我预计也不会起作用。隐式转换(与成员调用)中的规则是否有所不同?operator T()constconst volatileload()

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


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

use*_*522 19

Clang 和 MSVC 是正确的。

对于每个到类的函数指针的转换函数,都会将所谓的代理调用函数添加到重载决策中,如果选择该函数,将首先通过此运算符重载将对象转换为函数指针,然后通过函数指针调用该函数。这在[over.call.object]/2中进行了解释。

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

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

直接调用.load()-qualifiervolatile将成为重载决策中的决定性因素,因此不会出现此问题。

使用函数指针类型作为参数(*f)()对(内置)进行重载解析。operator*通过两个转换函数有两个隐式转换序列。标准对此不是很清楚,但我认为这样做的目的是,这不会导致不明确的转换序列(这也意味着选择时的重载解析不明确)。相反,我认为转换函数初始化的规则旨在仅选择其中一种转换,这将使其明确成为 -volatile限定的转换。