2

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.

alfC
  • 14,261
  • 4
  • 67
  • 118
  • You would need to save which f you wanted: `CombinedAB c; c.B::f(3.14);` – Jerry Jeremiah Sep 30 '20 at 03:28
  • @JerryJeremiah, yes, but that will force the user to know that `CombinedAB` derives from `B` specifically. Note that `f` could be resolved from the argument type. (I know this is the standard behavior, I am looking for a workaround.) – alfC Sep 30 '20 at 03:30
  • Have a look at this answer to a similar question https://stackoverflow.com/a/28349054/2193968 – Jerry Jeremiah Sep 30 '20 at 03:34
  • @JerryJeremiah, thanks for the pointer. It seems that the `BaseCollector` in that accepted answer is custom made for base classes with `foo`, I am trying to avoid that somehow. – alfC Sep 30 '20 at 03:40
  • Maybe https://stackoverflow.com/a/28349054/2193968 won't wotk for you either - it requires every base class to have the same functions: Here is an example for your question https://onlinegdb.com/SJZI1K-UD So now I am guessing that it isn't possible. – Jerry Jeremiah Sep 30 '20 at 03:46
  • In my experience, the purpose of a Mixin is to provide a specific interface without needing to know anything about the class using it. In other words, you would normally only access the Mixin functions when you are doing so via a pointer or reference using that Mixin's actual type. – paddy Sep 30 '20 at 03:54
  • If every base class has the same functions it works. If they don't then they need to specify the function as deleted to make it work: https://onlinegdb.com/HJ45ZFWLv So it works but doesn't fir your use case. – Jerry Jeremiah Sep 30 '20 at 03:57
  • @JerryJeremiah, thank you again for the link and code. The variadic `using` is a nice touch but the problem is still the same, the `BaseCollector` is designed specifically for `f`. (It should be called `fServiceCollector`.) See my update on a possible way around at the cost of a syntax change. – alfC Sep 30 '20 at 04:00
  • @paddy, I was actually trying to avoid runtime polymorphism (if that is what you are referring to) in favor of a static design. Even statically the design can arise by the use of generic code (template arguments). – alfC Sep 30 '20 at 04:02
  • 2
    Your update looks like an answer. Why not post it as an answer, instead of as an update to the question? – cigien Sep 30 '20 at 04:04
  • 1
    No, not at all, so long as there is no accepted answer, people will still answer. It's much better to keep the question and any answers separate. – cigien Sep 30 '20 at 04:12

1 Answers1

1

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.

alfC
  • 14,261
  • 4
  • 67
  • 118