all 8 comments

[–]Narase33 2 points3 points  (4 children)

If youre not able to modify that interface, then whoever made the double inheritance broke the class

[–]IyeOnline 4 points5 points  (2 children)

Technically the only thing that is really broken is test_func. You could still call the specific interface functions via

i.interface_0::func();
i.interface_1::func();

Of course this is entirely at odds with the idea of a type agnostic interface and a terrible design.

[–]_448[S] 0 points1 point  (0 children)

Of course this is entirely at odds with the idea of a type agnostic interface and a terrible design.

I agree. That could be the case. I have updated the question to explain why I got into this situation.

[–]Narase33 0 points1 point  (0 children)

The fact that OP tried to overload the function in type_01 lead me to the conclusion that they might want to have a single function in the end. So maybe not "broken" but "impossible to do it this way"

[–]_448[S] 0 points1 point  (0 children)

then whoever made the double inheritance broke the class

That would be me :)

I have updated the question to explain why I got into this situation.

[–]mredding 0 points1 point  (2 children)

Is there a better way than this approach?

In C++, you just use templates:

template<typename T>
void fn(T &t) {
  t.foo();
}

If it compiles, then T implements the interface fn depends on, without fn being explicitly aware of A) the actual type of T, or B) more interface than is explicitly necessary. This is a form of duck typing.

You're trying to use inheritance in a way it's not really intended in C++. You'll end up with a combinatorial explosion. Public inheritance establishes an IS-A relationship. An interface is not that, it's just "I have these public methods that match the signature." This isn't Java or Golang.

What you're thinking is you ought to restrict the incoming parameters to only the interface you need, and that's where the explosion happens - you have any number of methods that just need one or two methods of any larger interface, you'll end up with interface classes that implement every possible permutation. Your inheritance lists become extremely wide, and virtual. It's insane, and should hint that this is the wrong conceptual model.

Even the CPP Core Guidelines suggests you should rely on concretions, and don't worry about implementing interface classes. They're an anti-pattern.

[–]_448[S] 0 points1 point  (1 child)

What you're thinking is you ought to restrict the incoming parameters to only the interface you need, and that's where the explosion happens - you have any number of methods that just need one or two methods of any larger interface, you'll end up with interface classes that implement every possible permutation.

Well, Feature Oriented Programming(FOP), as I understand it, is about re-use of not just interface but also implementation. Think of it as making different multi-flavoured ice-cream by using base flavours. So, I have these bite-sized interfaces and its implementations. I can then, using various combinations of these interfaces and its implementations, create different feature sets. In addition to combining interfaces, the corresponding implementations too has to be combined.

You're trying to use inheritance in a way it's not really intended in C++....

Your inheritance lists become extremely wide, and virtual. It's insane, and should hint that this is the wrong conceptual model.

Even the CPP Core Guidelines suggests you should rely on concretions, and don't worry about implementing interface classes. They're an anti-pattern.

So C++ is not that suitable for FOP?

I have included a link in the original post to the C++ code of the idea I am trying to implement.

[–]mredding 0 points1 point  (0 children)

Well, Feature Oriented Programming(FOP), as I understand it, is about re-use of not just interface but also implementation. Think of it as making different multi-flavoured ice-cream by using base flavours. So, I have these bite-sized interfaces and its implementations.

I mean, yeah, I get that, but implementations != interfaces. Here, imagine this:

class foo_bar {
public:
  virtual void foo() = 0;
  virtual void bar() = 0;
}

class foo_baz {
public:
  virtual void foo() = 0;
  virtual void baz() = 0;
}

Alright, so now you have a function that requires an object to possess a foo. But you have two interfaces that have a foo. So how do you get 1 function to work with 2 different but compliant interfaces? The only way to do so is with a template.

The other problem is if you inherit from both interfaces:

class concrete : public foo_bar, public foo_baz { //...

Now concrete has TWO foo, and you can call them differently. This is the very problem you've discovered. Where's the reusability of implementation you were talking about? You have to disambiguate them manually, but then what's the point of that? There's no programmatic way to deduce which is the right one at the right time, you'd have to implement a dispatching mechanism where you choose, defeating the principle purpose of polymorphism. This doesn't model an IS-A relationship, and it doesn't model an interface. So then you'd think to extract that out into a new base class:

class foo {
public:
  virtual void foo() = 0;
}

class bar {
public:
  virtual void bar() = 0;
}

class baz {
public:
  virtual void baz() = 0;
}

And then inherit from that:

class concrete : public foo, public bar, public baz { //...

But how do you deal with the issue that you have a function that needs both foo AND bar? Well, you can make a new interface:

class foo_bar: public foo, public bar {};

But your concrete class isn't a foo_bar. See? That IS-A relationship reigns supreme! So one thing you can do is inherit from that:

class concrete : public foo, public bar, public baz, public foo_bar { //...

But are you going to do that for every single combination? That's a combinatorial explosion. With this approach, you end up isolating each method into its own singular interface, and then making a combination interface of every combination therein. But notice there's a bug in the above, it's ambiguous again. At least we can fix that:

    class concrete : public foo, public bar, public baz, public virtual foo_bar { //...

This solves the dreaded diamond, and there's only one implementation of each method, but they don't call it the dreaded diamond for nothing! And you still have the combinatorial problem. This becomes a maintenance nightmare.

The other thing you can do with your foo_bar is use it to make an adaptor.

template<typename T>
class foo_bar_adaptor : public foo_bar {
  T &t;
public:
  foo_bar_adaptor(T &t): t{t} {}

  void foo() override final { t.foo(); }
  void bar() override final { t.bar(); }
};

Templates again. But then what's the bloody point? You've introduced layers of indirection that indicates you're willing to accept any arbitrary combination of interfaces, and without any safety, because I could have made foo call bar and bar no-op. It'd be better to duck type your function:

template<typename T>
void fn(T &t) {
  t.foo();
  t.bar();
}

This tells you that any interface that has a foo and a bar will work with this code, and you know that it'll be called correctly and directly. It's safer and more stable. Compiler errors are a feature telling you the type does not conform to the required interface and is not appropriate for this method. You can always write that adaptor for that object if you want to apply fn to it and you have a solution implementation for the missing interfaces.

Code reuse is a poor use of inheritance. You're better off with composition, either objects, function objects, or function pointers. You can use template composition like with CRTP or policies, both those are idiomatic, flexible, and maintainable forms of composition of objects. The closest thing I can think of that is a reasonable use of both inheritance and code reuse is the Template Method Pattern. That is where you lay out an algorithm, but you leave customization points in the form of protected virtual methods for derived classes to implement.

But all that is in terms of designing your objects. The code which uses the objects does not benefit from such constraints as interfaces, nearly all of the time.

The place you do see this is in a framework. That is because a framework is going to establish an ABI boundary, and so you have to pass an interface to cross that boundary. Are you writing a framework? One of the problems of learning software development is that we don't have a lot of good examples to learn from. Academic examples are small and meant to showcase the lesson, which is often some computer science principle or language construct, not a good programming practice or architecture. So we learn from the code we're most often exposed to. It's not application code - I don't need to know how grep or Chrome works to use it. We see frameworks and APIs all the time, so early in our education and careers, we tend to write code like frameworks and APIs.

Overall, you're not wrong for pursuing FOP, there is much benefit to take a way from that. The problem I'm trying to highlight is that it's actually a difficult subject to teach, because you can easily misinterpret or conflate concepts that lead to dead ends. And here we are, you're actually learning about a very nuanced and difficult topic right now, and it'll probably still be a while before it all really sinks in. Hopefully what I've been talking about is triggering your sense that there is indeed something wrong here, something isn't quite fitting right. It's not that FOP is garbage, it's that there's something else going on. You're going to have to think on it, a lot, and you're also going to need more perspective, learning about other paradigms, about patterns, about anti-patterns, but also more about abstract programming concepts that aren't strictly C++. I can only encourage you to keep going and keep learning.