12

Of course you can, I'm just wondering if it's rational to design in such a way.

I'm making a breakout clone and was doing some class design. I wanted to use inheritance, even though I don't have to, to apply what I've learned in C++. I was thinking about class design and came up with something like this:

GameObject -> base class (consists of data members like x and y offsets, and a vector of SDL_Surface*

MovableObject : GameObject -> abstract class + derived class of GameObject (one method void move() = 0; )

NonMovableObject : GameObject -> empty class...no methods or data members other than constructor and destructor(at least for now?).

Later I was planning to derive a class from NonMovableObject, like Tileset : NonMovableObject. I was just wondering if "empty" abstract classes or just empty classes are often used...I notice that the way I'm doing this, I'm just creating the class NonMovableObject just for sake of categorization.

I know I'm overthinking things just to make a breakout clone, but my focus is less on the game and more on using inheritance and designing some sort of game framework.

6 Answers 6

4

In C++ you have Multiple Inheritance so there is literally no benefit to having those empty classes. If you want to have Ball inheriting from GameObject and MovableObject later (say, for example, you want to hold an array of GameObjects and call a Tick method every nth of a second to make them all move), that's easy enough to do.

But, in that situation, I would personally suggest that you remember the axiom "prefer composition (encapsulation) over inheritance" instead and look at the State pattern. If you want each object to have its movement state later, pass it to the GameObject. For example: DiagonalBouncingMovementState for the ball, HoriazontalControlledMovementState for the paddle, NoMovementState for a brick.

  1. This will make it easier for you to write unit tests, if you choose to (which, again, I would recommend) - because you can test GameObject and each of your states independently.

  2. Say you have tokens dropping from the bricks but then you want to change one of them so that they move diagonally - rather than shifting it around some complex hierarchy of class inheritance, you can just change the hardcoded movement state which gets passed to it.

  3. Most importantly: When you want to start changing in-game how the ball and/or paddle move based on those tokens, you just keep changing its movement state (DiagonalMovementState becomes DiagonalWithGravityMovementState).

Now, none of this was to suggest that inheritance is always bad, just to demonstrate why we prefer encapsulation over inheritance. You may still want to derive each type of object from GameObject and have those classes understand their initial Movement State. You will almost certainly want to derive each of your movement states from an abstract MovementState class because C++ doesn't have interfaces.

$0.02

14

In Java, "empty" interfaces are used as markers (e.g. Serializable), because at runtime, objects can be checked wether or not they "implement" that interface; but in C++, it seems pretty pointless to me.

IMO, language features should be considered tools, something that helps you reach certain goals, and not obligations. Just because you can use a feature doesn't mean you have to. Meaningless inheritance doesn't make a program better.

3

You are not over thinking it; you are thinking and that is good.

As already stated, don't use a language feature just because it's there. Learning language feature tradeoffs are key to good design.

One should not use base classes for categorization. That could implicate checking type. That is a bad idea.

Consider writing pure abstractions that define your objects' contract. Your objects' contract is the outward facing interface. Its public interface. What it does.

Note: It is important to understand this is not what data it has x, y but what it does move().

In your design, you have a class GameObject. You are relying on it for its data and not its functionality. It feels efficent and correct to use inheritance for sharing what seems like common shared data, in your case x and y.

This is not the correct use of inheritance. Always remember, inheritance is the tightest form of coupling you can possibly have and you should strive for loose coupling at all times.

By its very nature NonMovableObject doesn't move. Its x and y should most certainly be declared const. By its very nature MoveableObject needs to move. It cannot declare its x and y as const. Thus, this is not the same data and it cannot be shared.

Consider your objects' functionality, or contract. What should they do? Get that right and the data will come. Work on one object at a time. Worry about each in turn. You will find your good designs are reusable.

Perhaps there isn't a NonMovableObject at all. What about a pure abstraction GameObjectBase that defines a move() method? Your system instanciates GameObjects which implement move() and the system only moves the ones that need to move.

This is just the beginning; a taste. The rabbit hole goes much deeper. There is no spoon.

2
  • While it's true that neither MovableObject nor NonMovableObject can inherit from each other, they both have a location; as such, they should both be consumable by code which e.g. wants to direct a monster's aim toward an object without regard for whether that object might move or not.
    – supercat
    Commented Mar 11, 2014 at 21:09
  • The problem with declaring data members const is that you can't use std::vector<NonMovableObject> etc
    – Caleth
    Commented Feb 22, 2021 at 9:14
2

It has applications in C# like ammoQ mentioned, but in C++ the only application would be if you wanted to have a list of NonMovableObject pointers.

But then I'd imagine you'd be destructing the objects through the NonMoveableObject pointer, in which case you'd need virtual destructors, and it wouldn't be an empty class any more. :)

2
  • I thought about that case, too, but what could you do with such a list of objects that expose no behaviour? Most likely, you would somehow find out the real type and use a cast to access the available methods and fields - definitely a code smell.
    – user281377
    Commented Apr 6, 2011 at 8:48
  • Yes that's a good point.
    – tenpn
    Commented Apr 6, 2011 at 12:42
1

One situation where you might see this is where you want a strongly tyuped collection to contain otherwise heterogeneous types. I'm not saying this a great idea, it may indicate a weak abstraction or poor design, but it happens. As you mention it is a way of categorising stuff and it can be useful and perfectly valid.

E.g. You want a collection to hold Cats and Dogs. In your design you decide they have a lot in common so have a base class of Mamal and use this type in your collection (e.g. List). All good. Then you decide you want to add some Frogs to your collection but they are not mamals, so one way of doing this would be to make Frogs and Mamals subclass of Animal, but maybe you can't think of much the Frogs and Mamals have in common so Animal stays empty. You then type your collection with Animal instead of Mamal and all is well.

In real life I find that even if I do create an empty base class sooner or later some functionality ends up in there, but there are certainly times when they exist.

3
  • One question to ask, though, is: if the types have nothing in common, why are they being held in the same collection? There is no operation you can perform on all of the items in the collection, which kind-of defeats the purpose of having such a collection. Now there may be some artificial operations you might want to add to both to simplify logic -- e.g. implementing the Visitor design pattern -- at which point it might become useful again, but now they have something in common...
    – Jules
    Commented Mar 14, 2013 at 0:49
  • I've seen this for service locators. All services implementing IService, for example, but they get fetched by a generic method that gives you the concrete type instead. Purpose? So that only things intended to be used as services can be registered in the locator. How useful that is, YMMV.
    – Errorsatz
    Commented Feb 22, 2021 at 22:52
  • Jules, why wouldn’t they be in the same collection? I store completely unrelated things in my physical wardrobe. Or in a warehouse. Some software might want a list of everything in my wardrobe or my warehouse. Without any knowledge whatsoever what these items are. Swift has a protocol “Any” which can hold anything and you can have an array of “Any” or a dictionary with values of type “Any”.
    – gnasher729
    Commented Dec 28, 2022 at 23:48
0

Empty bases classes are a perfect fit for declaring compile-time interfaces on codebases that cannot rely on C++20 concepts. Empty base optimizations will ensure this will not affect any alignment requirements of the derived class.

The way I typically do this is by defining these empty compile-time interfaces using the CRT pattern, which are by themselves a widely used example of empty bases:

template< typename Impl >
class CTInterface
{
  static_assert(hasSomeTrait<Impl>, "Impl does not implement some trait") ;
} ;

class Impl : public CTInterface<Impl>
{} ;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.