Combine capabilities of classes (mixins) with ambiguous calls

177 views Asked by At

I was reading about the "mixin" technique in C++ but there is something I don't understand and seems to be a limitation in the language that prevents to do it generically because of ambiguities that the compiler (and the standard refuse to resolve, even if it can).

The idea of mixin is to aggregate capabilities from different parts. Sometimes the capabilities could be called with the same name because they generically do the same thing.

struct A{
    void f(int){}
};

struct B{
    void f(double){}
};

I could combine the services of the two classes with

struct CombinedAB : A, B{};

However when I use it I get a compilation error: "request for member ‘f’ is ambiguous" (godbolt link).

CombinedAB c; c.f(3.14);  // 3.14 is a double, I expect B::f to be called.

So, the compiler knows that f exists but it refuses to resolve which one is the correct one to call. It generates this error:

main.cpp: In function ‘int main()’:
main.cpp:23:21: error: request for member ‘f’ is ambiguous
     CombinedAB c; c.f(3.14);
                     ^
main.cpp:16:10: note: candidates are: void B::f(double)
     void f(double){}
          ^
main.cpp:12:10: note:                 void A::f(int)
     void f(int){}
          ^

Of course I could change the class and use this idiom "to bring a full fledged version of the f overloads".

struct CombineAB : A, B{
    using A::f;
    using B::f;
};

And it works, but the problem is that the CombineAB cannot be done generically. The closest would be:

template<class T1, class T2>
struct combination : T1, T2{
 // using T1::all_provided_by_T1;
 // using T2::all_provided_by_T2;
};

But I cannot not add the equivalent of using T1::f; using T2::f, first of all because the combination would need to know about all the possible functions in T1, T2.

So it seem that a combination would need to be defined in cases by case basis. For example with

template<class T1, class T2>
struct combination_of_f_service : T1, T2{
  using T1::f; using T2::f;
}

This defeats the purpose because if T1 and T2 provide 20 functions with the same name (as in my case) the class would need to repeat all these functions names. Worst of all it cannot be done via additional template arguments (can it?).

Is this the correct way to combine classes in C++?, is there a workaround or is this simply a too naive view of the problem? I am willing to accept an option involving quite a lot of template code if necessary.


The problem persists even if instantiation is not part of the equation and f is a static function. It seems that the language feels strongly about changing or combining the meaning of member functions in a base class.

1

There are 1 answers

0
alfC On

A partial answer to my own question:

As a matter of fact I am realizing that this is a peculiarity of the "dot" notation and how adamant is C++ with this syntax. It doesn't seem to be a fundamental limitation but a block that was put on purpose in the language to restrict the use of the dot notation. Maybe this is a historical quirk related to the possible misuse of virtual functions.

As soon as I let the "dot" notation go, the mixin behavior is suddenly possible with an equivalent (but somewhat unexpected) syntax.

struct A{
    void f(int){}
    friend void f(A& self, int i){self.f(i);}
};

struct B{
    void f(double){}
    friend void f(B& self, double d){self.f(d);}
};

struct C : A, B{
//    using A::f;
//    using B::f;
};

template<class T1, class T2>
struct combination : T1, T2{};

int main(){
    C c;
//    c.f(5.1); // error, ambiguous call
    f(c, 5.1);

    combination<A, B> c2;
    f(c2, 5.1);
}

It is interesting to note also that if this proposal goes forward https://isocpp.org/blog/2016/02/a-bit-of-background-for-the-unified-call-proposal, this issue will need to be resolved somehow. It is the first case in which I see that forcing the notation c.f(a) is fundamentally more limiting that forcing the f(c, a) notation. My conclusion is that mixins do not work through the "dot" notation. It is a pity that it affects static member functions as well ("::" notation).

I would still be interested in some technique to use the "dot" notation if any body knows.