Ambiguous call operator overload weirdly resolved by introducing unrelated call operator - clang vs gcc

Viewed 85

Consider the task of implementing an overload_two class so that the following code compiles:

int main()
{
    overload_two o{
        [](int) { return 1; },
        [](char){ return 2; }
    };

    static_assert(o(0) + o('a') == 3);
}

A first attempt would look something like this:

template <typename F, typename G>
struct overload_two : F, G
{
    overload_two(F f, G g) : F{f}, G{g} { }
};

Of course, this fails with the following error (gcc 11.0.1):

<source>:15:22: error: request for member 'operator()' is ambiguous
   15 |     static_assert(o(0) + o('a') == 3);
      |                      ^

<source>:12:9: note: candidates are: 'main()::<lambda(char)>'
   12 |         [](char){ return 2; }
      |         ^

<source>:11:9: note:                 'main()::<lambda(int)>'
   11 |         [](int) { return 1; },
      |         ^

The intended solution is to bring both F::operator() and G::operator() into scope with using declarations. However -- very surprisingly -- introducing a seemingly unrelated operator() also works:

template <typename F, typename G>
struct overload_two : F, G
{
    overload_two(F f, G g) : F{f}, G{g} { }
    auto operator()(F&) { }  // wtf?
};

live example on godbolt.org


What's more, clang 13.0.0 seems to work without neither the using declaration or the random operator(): live example on godbolt.org.


My questions are:

  1. Is gcc correct in rejecting the implementation of overload_two without any operator() or using declaration?

  2. Is gcc correct in accepting the implementation of overload_two with operator()(F&) and no using declaration?

  3. Is clang correct in accepting the implementation of overload_two without any operator() or using declaration?

1 Answers
  1. Yes. Name lookup for the name operator() is ambiguous because it found the name in both bases (see [class.member.lookup]/5.2). That's an error right there, don't pass GO, don't collect $200.
  2. Yes. The unrelated operator() makes name lookup succeed by hiding the base class members. Because your lambdas are captureless, they are implicitly convertible to function pointers, and these implicit conversions are inherited by the derived class. When your class is convertible to function pointers (or references), overload resolution will additionally generate surrogate call functions from these conversions. Those will be selected for the calls in the example.
  3. No. See #1.
Related