Discussion: The Phantom Spaceship
Execute the code to understand the output and gain insights into object construction and virtual function behavior.
Run the code
Now, it’s time to execute the code and observe the output.
#include <iostream>#include <string>struct GameObject{GameObject() { std::cout << "Created a " << getType() << "\n"; }void render() const { std::cout << "Rendered a " << getType() << "\n"; }virtual std::string getType() const { return "GameObject"; }};class Spaceship : public GameObject{std::string getType() const override { return "Spaceship"; }};void display(const GameObject &gameObject) { gameObject.render(); }int main(){GameObject gameObject;Spaceship spaceship;display(gameObject);display(spaceship);}
Understanding the output
In this puzzle, let’s say we’ve attempted to add logging to our game to keep track of the creation and rendering of game objects. We’ve decided to use the template method design pattern, which can be a great way to achieve runtime polymorphic behavior in our design. In this pattern, a base class defines common behavior in a concrete function, which internally calls virtual functions that can be overridden in derived classes to fill in the details. For example, the preceding render()
function is a template method that calls the virtual getType()
function to fill in the details of the specific derived type.
This works perfectly for the render()
function, but something goes wrong in the constructor; it prints Created a GameObject
both when creating a GameObject
and when creating a Spaceship
. What exactly is going on, and could this be undefined behavior?
Constructor’s behavior
In main
, when initializing an object of type Spaceship
, the GameObject
base class part of the object is initialized first, and then the Spaceship
part of the object is initialized. During the initialization of the GameObject
part, there’s no Spaceship
part yet. As far as C++ is concerned, this is simply a GameObject
being created.
So what happens when we call the virtual getType()
member function in Spaceship
constructorSpaceship
which doesn’t even exist yet? Undefined behavior? Luckily, this behavior is well-defined. When a virtual function is called from a constructor or destructor, the function called is the one in that particular class and not one overriding it in a more-derived class. So, in the GameObject
constructor, GameObject::getType()
is called both when creating a GameObject
and when creating a Spaceship
. In both cases, Created a GameObject
is displayed.
Rendering behavior
In render
, however, we’re dealing with fully initialized objects, and the virtual function works as intended. When we render the GameObject
, Rendered a GameObject
is printed, and when we render the Spaceship
, Rendered a Spaceship
is printed.
Fixing the issue
So how do we fix this? Before we look into ways of making the actual type being instantiated available to the GameObject
constructor, it’s worth asking whether it should even be possible to instantiate GameObject
objects on their own. Does it make sense in the game to be able to instantiate an abstract game object? Or does it only make sense to instantiate concrete things like, say, a spaceship, an asteroid, or an astronaut? If we make GameObject::getType()
a pure virtual function rather than giving it a definition, GameObject
is now an abstract class, which cannot be instantiated on its own:
virtual std::string getType() const = 0;
Impact of pure virtual functions
How does making GameObject::getType()
a pure virtual function affect this program, though? If we tried it, we might already have noticed that creating a GameObject gameObject
no longer compiles. We can’t instantiate objects of abstract classes, only of concrete derived classes like Spaceship
. Additionally, creating a Spaceship spaceship
now results in undefined behavior since the base class GameObject
constructor calls a pure virtual function. In practice, we can expect to see either compiler warnings for the call to the pure virtual function, linker errors about a missing GameObject::getType()
definition, or runtime errors like “Pure virtual function called.” But, as always, with undefined behavior, this varies between platforms and compilers. Is going from the wrong result to undefined behavior an improvement, though? In this case, we think so since, given a decent CI setup, we’re likely to discover the problem and fix it.
Proper fix
How do we fix this properly, then? Since there’s no way for the GameObject
constructor to know which derived type is actually being constructed, we have to somehow provide it with that information. The easiest solution is to pass the information directly to the constructor with a constructor parameter like so:
protected:GameObject(std::string_view type) {std::cout << "Created a " << type << "\n";}
This new constructor is protected since it’s only intended for being called from derived classes. We do that in Spaceship
like so:
public:Spaceship() : GameObject("Spaceship"){}
Note that the Spaceship
string is now duplicated between the constructor and the getType()
function and should probably be extracted to a static constexpr
member of the Spaceship
class.
If we still want to be able to instantiate raw GameObjects
, however, we need to add back a default constructor:
public:GameObject() : GameObject("GameObject"){}
This introduces a new problem, though: if we forget to add a user-defined constructor for Spaceship
, the defaulted Spaceship
constructor will simply use the GameObject
default constructor, and we’re back to seeing GameObject created
when creating a SpaceShip
—yet another reason not to allow GameObjects
to be independently constructed.
#include <iostream>#include <string>struct GameObject{protected:GameObject(std::string_view type) {std::cout << "Created a " << type << "\n";}public:GameObject() : GameObject("GameObject") {}void render() const { std::cout << "Rendered a " << getType() << "\n"; }virtual std::string getType() const { return "GameObject"; }};class Spaceship : public GameObject{public:Spaceship() : GameObject("Spaceship") {}std::string getType() const override { return "Spaceship"; }};void display(const GameObject &gameObject) { gameObject.render(); }int main(){GameObject gameObject;Spaceship spaceship;display(gameObject);display(spaceship);}
Consider the curiously recurring template pattern (CRTP)
Finally, when the issue is how to use information from a derived class in a base class without using dynamic polymorphism, no discussion is complete without at least a mention of the curiously recurring template pattern (CRTP) idiom. While we don’t have space for a detailed discussion here, the idea is to make the base class a template and then use the derived class as a template parameter for its own base class. Now, the base class knows, statically even, which derived class is being instantiated simply by looking at its template parameter! Here’s a full example:
#include <iostream>struct GameObject {virtual void render() const = 0;};template<typename Derived>struct LoggingGameObject : public GameObject{LoggingGameObject(){std::cout << "Created a " << Derived::typeName << "\n";}void render() const override{std::cout << "Rendered a " << Derived::typeName << "\n";}};struct Spaceship : public LoggingGameObject<Spaceship>{static constexpr auto typeName = "Spaceship";};void display(const GameObject &gameObject) { gameObject.render(); }int main(){Spaceship spaceship;display(spaceship);}
Recommendations
Here are some recommendations to ensure robust and maintainable C++ code.
Enable warnings: Compilers like GCC and Clang can, for instance, warn about calls to pure virtual functions in constructors and destructors.
Use several compilers and platforms in CI: Different compilers and linkers give different warnings and errors.
Level up your interview prep. Join Educative to access 70+ hands-on prep courses.