19

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.

2
  • 1
    All the compilers like (*f)(); but I am not sure what the difference is - I guess it's the same as f.load()() (as above). Commented Apr 28, 2022 at 23:57
  • @RichardCritten That's interesting, though I think (*f)() is more like f(), because operator T() is used implicitly for both (whereas there's no conversion in f.load()()). Commented Apr 29, 2022 at 0:16

1 Answer 1

20

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.

Sign up to request clarification or add additional context in comments.

12 Comments

@ixSci If the object is volatile-qualified, then overload resolution shouldn't be ambiguous. The surrogate call function is generated only for conversion functions which are at least as cv-qualified as the object is. (See linked standard reference.) So MSVC is wrong then.
@ja2142 In "normal" overload resolution on a member function call the cv-qualifier of the function works by accordingly qualifying the implicit object parameter of the function. The conversion of the object expression to the implicit object parameter then has the mentioned tie-breaker applied.
In the case of the surrogate call function, the first parameter of the function submitted for overload resolution is not the implicit object parameter, but instead a parameter of the target function pointer type. This type already has its own cv-qualifications. So there is no place to sensibly add the cv-qualifiers of the conversion function. I am not sure there is any simple solution to this issue. Maybe some additional explicit tie-breakers for surrogate call functions, or std::atomic could add proper operator() overloads.
@ja2142 For overload resolution member functions are considered the same as non-member functions, except that an additional first parameter is added to them, called the implicit object parameter, which is a reference to the class type qualified according to the cv-qualifiers and ref-qualifiers of the member function. Since the surrogate call function "exists" only for the purpose of overload resolution, it isn't really useful to ask whether it is a member. It is not a member at least in the sense that the aforementioned transformation is not applied.
OK, I'm stupid. #1) where does volatile enter into it at all? (There's no such keyword in the example and I don't see any in std::atomic over there at cppreference either.) #2) Doesn't this mean you can't use the f() syntax to call any 0-arg function through an atomic pointer?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.