ja2142
ja2142

Reputation: 1038

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

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 additionally specifies possible call candidates to be:

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.

Upvotes: 18

Views: 1190

Answers (1)

user17732522
user17732522

Reputation: 76809

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.

Upvotes: 19

Related Questions