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.

Press + to interact
#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 the Spaceship constructorWhen we create an instance of a derived Spaceship class, C++ first calls the default constructor of the base GameObject class. This happens even if we do not explicitly define a constructor in the derived class., which would normally call down to a Spaceship 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";
}
Passing derived type information to base class constructors

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"){}
Using a constructor parameter to identify derived types

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"){}
Handling the default constructor

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.

Press + to interact
#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:

Press + to interact
#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.