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

Time:04-29

When calling function from an atomic function pointer, like:

#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 doesn't complain at all, while clang and msvc have problem with call f():

  • [clang]: error: call to object of type 'std::atomic<func_type>' (aka 'atomic<int (*)()>') is ambiguous
  • [msvc]: there is more than one way an object of type "std::atomic<func_type>" can be called for the argument list

Clang additionally specifies possible call candidates to be:

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

It seems like this difference in volatility is confusing for clang and msvc, but not gcc.

When call is changed from f() to f.load()(), the code works in all abovementioned compilers. Which is all the more confusing, since both load() and operator T() are said to have const and const volatile overloads - if implicit conversion doesn't work, I'd expect load() not to work as well. Are the rules somehow different within implicit conversions (versus member calls)?

So, is gcc wrong to accept that code? Are clang and msvc wrong to error out? Any other combination of being wrong or right?


This is mostly a theoretical question, but if there is some better way to have an atomic function pointer, I'd like to know.

CodePudding user response:

Clang and MSVC are correct.

For each conversion function to a function pointer of the class, a so-called surrogate call function is added to overload resolution, which if chosen would first convert the object via this operator overload to a function pointer and then call the function via the function pointer. This is explained in [over.call.object]/2.

However, the surrogate call function does not translate the cv-qualifiers of the conversion operator in any way. So, since std::atomic has a conversion operator which is volatile and one which is not, there will be two indistinguishable surrogate call functions. These are also the only candidates since std::atomic doesn't have any actual operator() and so overload resolution must always be ambiguous.

There is even a footnote in the standard mentioning that this can happen, see [over.call.object]/footnote.120.

With a direct call to .load() the volatile-qualifier will be a tie-breaker in overload resolution, so this issue doesn't appear.

With (*f)() overload resolution on the (built-in) operator* with the function pointer type as parameter is performed. There are two implicit conversion sequences via the two conversion functions. The standard isn't very clear on it, but I think the intention is that this doesn't result in an ambiguous conversion sequence (which would also imply ambiguous overload resolution when it is chosen). Instead I think it is intended that the rules for initialization by conversion function are applied to select only one of the conversions, which would make it unambiguously the volatile-qualified one.

  • Related