0

I have looked high and low for answers to this question - here on this forum and on the general internet. While I have found posts discussing similar topics, I am at a point where I need to make some design choices and am wondering if I am going about it the right way, which is as follows:

In C++ I have created 3 data structures: A linked list, a binary tree and a grid. I want to be able to store different classes in these data structures - one may be a class to manipulate strings, another numbers, etc. Now, each of these classes, assigned to the nodes, has the ability to perform and handle comparison operations for the standard inequality operators.

I thought C++ inheritance would provide the perfect solution to the matter - it would allow for a base "data class" (the abstract class) and all the other data classes, such as JString, to inherit from it. So the data class would have the following inequality method:

virtual bool isGreaterThan(const dataStructure & otherData) const = 0;

Then, JString will inherit from dataStructure and the desire would be to override this method, since isGreaterThan will obviously have a different meaning depending on the class. However, what I need is this:

virtual bool isGreaterThan(const JString & otherData) const;

Which, I know will not work since the parameters are of a different data type and C++ requires this for the overriding of virtual methods. The only solution I could see is doing something like this in JString:

virtual bool isGreaterThan(const dataStructure & otherData);
{
  this->isGreaterThanJString(dynamic_cast<const JString&>(theSourceData));
};

virtual bool isGreaterThanJString(const JString & otherData) const;

In other words, the overriding method just calls the JString equivalent, down-casting otherData to a JString object, since this will always be true and if not, it should fail regardless.

My question is this: Does this seem like an acceptable strategy or am I missing some ability in C++. I have used templates as well, but I am trying to avoid this as I find debugging becomes very difficult. The other option would be to try a void* that can accept any data type, but this comes with issues as well and shifts the burden onto the code resulting in lengthier classes.

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
Jason
  • 3
  • 2
  • 2
    Templates, operator overloading, lambdas, and maybe `std::list` should get you on your way. – sweenish Sep 10 '21 at 23:46
  • Templates are imho the way to go here. Honestly I don't know why this makes debugging more complicated. Coding a template class can be a bit more troublesome since you're missing auto completion features and error checks usually done by the IDE, but these issues can be solved by writing a class, but you only mentioned debugging being an issue. – fabian Sep 11 '21 at 00:08
  • 1
    You're falling into the trap of over-using class inheritance, in a manner of "I have a hammer so everything I need to hit must be a nail". Public class inheritance makes sense for representing "is a" relationships between classes - not for inheriting operations that all derived classes must specialise and need to be aware of how those operations are done in all other derived classes. Your case is that some (largely unrelated) classes support some similar (generic) operations. In C++, templates, function overloading, and other mechanisms will be more viable approaches than class inheritance. – Peter Sep 11 '21 at 00:08
  • Thanks for the feedback on this - I agree it seems like templates are the appropriate solution here and I'll probably go back to that. As regards to Peter's post, that's what I was wondering about myself. I guess it is like creating a class of "living things" including all the millions of species on earth with there disparate characteristics and traits - in the programming world the "living things" class would be monstrously large. – Jason Sep 11 '21 at 18:44

1 Answers1

0

The LSP means operations on a reference to base class must work and have the same semantics as operations on both base and derived class instances when those operations are referentially polymorphic.

Your example fails this test. The base isGreaterThan claims to work on all dataStructure, but it does not.

I would make the dataStructure argument types templates in your containers. Then you know the concrete type of the stored data.

Look at std list for an idea of what a linked list template might look like.

I will now go onto complex additional steps you can do in the 0.1% of cases where the above advice is not correct.

If this causes issues, because of template bloat, you could create a polymorphic container that enforces the type of the stored data, either with a thin template wrapper or runtime tests. Once stored, you blindly cast to the known stored type, and store how to copy/compare/etc said type either in a C or C++ style polymorphic method.

Here is an 8 year old fun talk about this approach: https://channel9.msdn.com/Events/GoingNative/2013/Inheritance-Is-The-Base-Class-of-Evil

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524