3

I tried using std::is_invocable in the following code snippet available on godbolt.

#include <type_traits>
 
struct A {
  void operator()(int);
};

struct B {
  void operator()(int, int);
};

struct C : A, B {};

int main()
{
  static_assert(std::is_invocable<A, int>() == true);
  static_assert(std::is_invocable<B, int>() == false);
  static_assert(std::is_invocable<C, int>() == true);
}

Clang can figure it all out, but GCC seems to have problem with this line:

static_assert(std::is_invocable<C, int>() == true);

C has inherited function call operators.

So GCC's implementation is incorrect or still in progress?

rustyx
  • 80,671
  • 25
  • 200
  • 267
  • 1
    It surprises me to see a compiler divergence in such a mundane case, but your test can be simplified to Clang and GCC having different opinions on whether `C{}(42)` is ambiguous or not. – Quentin Dec 10 '21 at 16:04
  • Better link to [godbolt](https://godbolt.org/z/fTx18qdTe) which shows problem with gcc. – Marek R Dec 10 '21 at 16:06
  • @Quentin Why does GCC even consider `C{}(42)` ambiguous? I don't see how it could ever match the `operator()(int, int)` case. – orlp Dec 10 '21 at 16:06
  • @orlp for the same reason that defining a function in a child class does not overload a parent class function of the same name, but hides it: they are considered separate overload sets until you use `using` to bring the parent overloads into the child's overload set. This happens before overload resolution considers the provided arguments. I do believe this is the correct behaviour, although I haven't checked against the standard. – Quentin Dec 10 '21 at 16:10
  • 1
    Per [this](https://stackoverflow.com/questions/1313063/request-for-member-is-ambiguous-in-g), it looks like GCC is actually correct here. ICC also says the call is ambiguous. – NathanOliver Dec 10 '21 at 16:11
  • Functional workaround: https://godbolt.org/z/E8ss3dnsx – Marek R Dec 10 '21 at 16:20
  • Plus, in the case of a method rather than a call operator, both clang and gcc would fail to compile due to ambiguity ([godbolt](https://godbolt.org/z/Ps7qbbKbW)). I don't think the `using` statement or the [overloaded pattern](https://en.cppreference.com/w/cpp/utility/variant/visit) would exist if it weren't a clang bug – parktomatomi Dec 10 '21 at 16:23
  • If function is not an operator all compilers are complaining about ambiguity https://godbolt.org/z/9xe61P63P – Marek R Dec 10 '21 at 16:27

1 Answers1

5

GCC is actually correct here. If we follow the rules in [class.member.lookup] we first have

The lookup set for N in C, called S(N,C), consists of two component sets: the declaration set, a set of members named N; and the subobject set, a set of subobjects where declarations of these members were found (possibly via using-declarations). In the declaration set, type declarations (including injected-class-names) are replaced by the types they designate. S(N,C) is calculated as follows:

So we are going to build a set of names S(N, C) for the class (also named C in this case)

Moving on to the next paragraph we have

The declaration set is the result of a single search in the scope of C for N from immediately after the class-specifier of C if P is in a complete-class context of C or from P otherwise. If the resulting declaration set is not empty, the subobject set contains C itself, and calculation is complete.

and when we do the lookup of operator() in C, we don't find any, so S(N, C) is empty. We then move on to the next pargraph

Otherwise (i.e., C does not contain a declaration of N or the resulting declaration set is empty), S(N,C) is initially empty. Calculate the lookup set for N in each direct non-dependent ([temp.dep.type]) base class subobject Bi, and merge each such lookup set S(N,Bi) in turn into S(N,C).

So, we are going to go through each base and add it's lookup set into S(N, C) which brings us to the next sub paragraph of

Otherwise, if the declaration sets of S(N,Bi) and S(N,C) differ, the merge is ambiguous: the new S(N,C) is a lookup set with an invalid declaration set and the union of the subobject sets. In subsequent merges, an invalid declaration set is considered different from any other.

So, since there was no operator() in C, but it is in the bases, then we now have invalid declaration set that contains the base class operator()'s. Then we have

he result of the search is the declaration set of S(N,T). If it is an invalid set, the program is ill-formed. If it differs from the result of a search in T for N from immediately after the class-specifier of T, the program is ill-formed, no diagnostic required.

Which tells us that a invalid declaration set is ill-formed, so it is an error and should fail to compile.

The workaround for this is to bring the base class functions into the derived class scope utilizing a using statement like

struct C : A, B {
    using A::operator();
    using B::operator();
};

Which now brings the name into C which will give you a valid name set that can then be passed to overload resolution for it to pick the correct overload.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402