4 ms·
What do you think of the following: class AInterface { /*...*/ }; class BInterface : public AInterface { /*...*/ }; class CInterface : public BInterface
by MauranKilom 7y ago
What do you think of the following:
class AInterface { /*...*/ };
class BInterface : public AInterface { /*...*/ };
class CInterface : public BInterface { /*...*/ };
class AImpl : public AInterface { /*...*/ };
class BImpl : public BInterface { /*...*/ };
class CImpl : public CInterface { /*...*/ };
In this use case, assume going from AInterface to CInterface implies stronger and stronger guarantees that the implementations provide. Also assume LSP as satisfied.
Now, you are given an AInterface* object and want to know whether it has a certain guarantee or not. A dynamic cast answers this with minimal space overhead.
Yes, there is certainly run time overhead in the dynamic cast, and you could just have a virtual method that returns some encoding of the guarantees (e.g. as an enum), but the dynamic_cast is still pareto-optimal in terms of space.
Change my mind?
- brandmeyer 7y ago> Now, you are given an AInterface* object and want to know whether it has a certain guarantee or not. Eiffel guarantees this in the LSP sense, by enforcing that overrides `require` as much or less than their base class, and `ensure` as much or more than their base class. If you want to query that from AInterface alone, you should expose a virtual member that allows you to make the specific inquiry of interest from AInterface. In effect, if users of AInterface must know that only some AInterface's meet some guarantee, you must expose some knowledge of the guarantee in AInterface itself. For example, AInterface can expose a boolean predicates like supports_foo, or it can return an object that is comparable with a std::strong_ordering.
- MauranKilom 7y agoI fully agree, this is what I would go for in this case. The only caveat is that this is duplicating information that RTTI already has available, i.e. space overhead. That's not a strong argument, but still a pareto-optimal niche.
- jfkebwjsbx 7y agoGiven most people use C++ for its performance in time rather than in space, I think most projects would go with a member/virtual method.
- CoolGuySteve 7y agoI would argue that much like Go's aversion to deep inheritance hierarchies, this interface hierarchy would be annoying to use in practice. If A, B, and C are different families of functionality then ditch the inheritance and use separate interfaces for each family of functionality. The cascading dynamic_cast logic required is a code smell that this could be made simpler.
- cryptonector 7y ago> this interface hierarchy would be annoying to use in practice No doubt, but it happens, and sometimes because the refactoring you might prefer to do can't be done or can't be done right away. For example, in Java you might have a subclass of Principal where you store additional metadata and have additional methods, then you might have code like: public static Something princOk(Principal p, Stuff stuff) throws MyPrincipalException { if (p instanceof MyPrincipal) return princOk((MyPrincipal)p, stuff); return princOk(MyPrincipal(p), stuff); } Why would you do this? Well... because many JDK and third-party interfaces take Principal object arguments that you could reuse... if only you could decorate those objects with the additional metadata that you wanted, and... with subclassing you get to. In Java you might say "use Subject", but it's not that easy, and anyways, JAAS is a terrible terrible thing that should have been ripped out when applets were ripped out.
- CoolGuySteve 7y agoIn C++, you can use templates to pattern match/duck type interfaces at compile time. So "template<typename PrincType> void princOk(PrincType& p, Stuff& stuff) {p.doThing(stuff); }" would map properly as long as doThing was implemented in all the different classes. You don't actually need an interface or base class for it to work. The rest of the dynamic mapping is faster by calling switch() on an enum class member and than static_cast rather than relying on dynamic_cast.
- MauranKilom 7y agoIt actually works really well in practice for the example I'm thinking of (and this is deep in the bones of a code base, not just something I thought of for fun). Each functionality is a true subset of the other. It's not different families of functionalities. Also, I don't see how your suggestion would help with void foo(const AInterface& someObject); What type should foo take instead if it can handle all levels of guarantee but might engage better code paths the more guarantees are available? Would you use a sum type (which in C++ boils down to type erasure, i.e. polymorphy but hidden and costlier)? Would you overload it (and then have code duplication)? I'm not convinced you could make it simpler.