27

I'm playing with a trick to overload lambdas in C++. Specifically:

// For std::function
#include <functional>

// For std::string
#include <string>

// For std::cout
#include <iostream>

template <class... F>
struct overload : F... {
    overload(F... f) : F(f)... {}
};      

template <class... F>
auto make_overload(F... f) {
    return overload<F...>(f...);
}

int main() {

    std::function <int(int,int)> f = [](int x,int y) {
        return x+y;
    };
    std::function <double(double,double)> g = [](double x,double y) {
        return x+y;
    };
    std::function <std::string(std::string,std::string)> h = [](std::string x,std::string y) {
        return x+y;
    };

    auto fgh = make_overload(f,g,h);
    std::cout << fgh(1,2) << std::endl;
    std::cout << fgh(1.5,2.5) << std::endl;
    std::cout << fgh("bob","larry") << std::endl;
}

Now, the above program compiles and works fine in clang:

$ clang++ -g -std=c++14 test01.cpp -o test01
$ ./test01
3
4
boblarry

It does not compile in gcc:

$ g++ -g -std=c++14 test01.cpp -o test01
test01.cpp: In function 'int main()':
test01.cpp:36:25: error: request for member 'operator()' is ambiguous
     std::cout << fgh(1,2) << std::endl;
                         ^
In file included from test01.cpp:5:0:
/usr/lib/gcc/x86_64-pc-linux-gnu/4.9.2/include/g++-v4/functional:2434:5: note: candidates are: _Res std::function<_Res(_ArgTypes ...)>::operator()(_ArgTypes ...) const [with _Res = std::basic_string<char>; _ArgTypes = {std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >}]
     function<_Res(_ArgTypes...)>::
     ^
/usr/lib/gcc/x86_64-pc-linux-gnu/4.9.2/include/g++-v4/functional:2434:5: note:                 _Res std::function<_Res(_ArgTypes ...)>::operator()(_ArgTypes ...) const [with _Res = double; _ArgTypes = {double, double}]
/usr/lib/gcc/x86_64-pc-linux-gnu/4.9.2/include/g++-v4/functional:2434:5: note:                 _Res std::function<_Res(_ArgTypes ...)>::operator()(_ArgTypes ...) const [with _Res = int; _ArgTypes = {int, int}]
test01.cpp:37:29: error: request for member 'operator()' is ambiguous
     std::cout << fgh(1.5,2.5) << std::endl;
                             ^
In file included from test01.cpp:5:0:
/usr/lib/gcc/x86_64-pc-linux-gnu/4.9.2/include/g++-v4/functional:2434:5: note: candidates are: _Res std::function<_Res(_ArgTypes ...)>::operator()(_ArgTypes ...) const [with _Res = std::basic_string<char>; _ArgTypes = {std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >}]
     function<_Res(_ArgTypes...)>::
     ^
/usr/lib/gcc/x86_64-pc-linux-gnu/4.9.2/include/g++-v4/functional:2434:5: note:                 _Res std::function<_Res(_ArgTypes ...)>::operator()(_ArgTypes ...) const [with _Res = double; _ArgTypes = {double, double}]
/usr/lib/gcc/x86_64-pc-linux-gnu/4.9.2/include/g++-v4/functional:2434:5: note:                 _Res std::function<_Res(_ArgTypes ...)>::operator()(_ArgTypes ...) const [with _Res = int; _ArgTypes = {int, int}]
test01.cpp:38:35: error: request for member 'operator()' is ambiguous
     std::cout << fgh("bob","larry") << std::endl;
                                   ^
In file included from test01.cpp:5:0:
/usr/lib/gcc/x86_64-pc-linux-gnu/4.9.2/include/g++-v4/functional:2434:5: note: candidates are: _Res std::function<_Res(_ArgTypes ...)>::operator()(_ArgTypes ...) const [with _Res = std::basic_string<char>; _ArgTypes = {std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >}]
     function<_Res(_ArgTypes...)>::
     ^
/usr/lib/gcc/x86_64-pc-linux-gnu/4.9.2/include/g++-v4/functional:2434:5: note:                 _Res std::function<_Res(_ArgTypes ...)>::operator()(_ArgTypes ...) const [with _Res = double; _ArgTypes = {double, double}]
/usr/lib/gcc/x86_64-pc-linux-gnu/4.9.2/include/g++-v4/functional:2434:5: note:                 _Res std::function<_Res(_ArgTypes ...)>::operator()(_ArgTypes ...) const [with _Res = int; _ArgTypes = {int, int}]
Makefile:2: recipe for target 'all' failed
make: *** [all] Error 1

Why is there a difference? For the record, I'm using gcc 4.9.2 and clang 3.5.0.


Edit 1

Evidently, this snippet of code failed to compile on VC as well and had already been reported. That being said, Sean Middleditch posted a working version of the overloaded code:

template<class F1, class... Fs>
struct overload : F1, overload<Fs...>
{
    using F1::operator();
    using overload<Fs...>::operator();
    overload(F1 f1, Fs... fs) : F1(f1), overload<Fs...>(fs...) {}
};

template<class F1>
struct overload<F1> : F1
{
    using F1::operator();
    overload(F1 f1) : F1(f1) {}
};


template <class... F>
auto make_overload(F... f) {
    return overload<F...>(f...);
}

I'm still interested in understanding why this version of the overloaded lambda code works, but the original one does not.

Barry
  • 286,269
  • 29
  • 621
  • 977
wyer33
  • 6,060
  • 4
  • 23
  • 53
  • 1
    This may just be due to the simplifying of your code for this question, but since you're using C++14, `[](auto x, auto y){return x+y;}` would produce a lambda with the same overloaded capabilities. – Drew Dormann Apr 15 '15 at 21:33
  • 1
    @DrewDormann For sure. Really, I was just trying to come up with a single example to show what was going on. Later, I'd like to use it for more complicated cases. – wyer33 Apr 15 '15 at 21:37
  • It's surprising that the original code compiled in clang. The general rule is that the same names in different base classes do not overload. – T.C. Apr 15 '15 at 21:42
  • @T.C. operator lookup, maybe? – dyp Apr 15 '15 at 21:56
  • 3
    @dyp No, this case involves standard lookup of `operator()` in the context of `(fgh).operator()` ([over.call.object]/p1). The result of that lookup is clearly ambiguous. – T.C. Apr 15 '15 at 21:59
  • 2
    Simplified example: http://coliru.stacked-crooked.com/a/7470dd2f07b1aa14 – Brian Bi Apr 15 '15 at 22:11
  • 4
    Stop using `std::function` when is completely unnecessary. It is a bad practice and less experienced people will learn it from code like this. – sbabbi Apr 15 '15 at 23:42
  • 1
    You aren't overloading lambdas, you are overloading `std::function`s. Lambdas and `std::function`s are unrelated types. As an aside, I'd want to binary tree inherit myself, instead of linear, just to keep things cleaner. – Yakk - Adam Nevraumont Apr 16 '15 at 02:55

2 Answers2

16

Looks like a Clang bug to me.

The general rule is that member functions of the same name in different base classes do not overload. For example:

struct Foo { void bar(); };
struct Baz { void bar(int); };
struct Quux : Foo, Baz { };

int main() { Quux().bar(); } // error on both GCC and Clang

For whatever reason, Clang fails to diagnose this ambiguity for operator().

A using-declaration lifts the named base class members to the derived class scope, allowing them to overload. Hence:

struct Quux_2 : Foo, Baz { using Foo::bar; using Baz::bar; };
Quux_2().bar(); // OK.

In the working version of the code, the using declarations recursively bring every operator() declaration in the template arguments into the scope of the most derived class, allowing them to overload.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • This is because name lookup is done before overload resolution even starts, and name lookup finds both `Foo::bar` and `Baz::bar` of which neither is preferred? – M.M Sep 09 '15 at 21:07
10

The original code shouldn't compile, gcc is correct here. See [class.member.lookup]:

Otherwise (i.e., C does not contain a declaration of f or the resulting declaration set is empty), S(f,C) is initially empty. If C has base classes, calculate the lookup set for f in each direct base class subobject Bi, and merge each such lookup set S(f,Bi) in turn into S(f,C).
— [..]
— Otherwise, if the declaration sets of S(f,Bi) and S(f,C) differ, the merge is ambiguous...

The initial declaration set is empty (overload has no methods) - so merge all the bases, all of whom have differing sets. So the merge should fail. That rule only applies if the declaration set of overload is empty though, which is why the explicit adding of the using F1::operator() works.

Barry
  • 286,269
  • 29
  • 621
  • 977