The virtual
keyword in C++ is the key that unlocks dynamic dispatch, which is essential for polymorphism when working with pointers or references to base classes.
Consider a simple function call like myObject.SomeFunction();
. When the compiler sees this, it knows the exact type of myObject
at compile time. It can directly determine which function (MyClass::SomeFunction
) to call. This is called static dispatch or early binding.
Now, consider the scenario from the lesson:
// Assume Rectangle has Render()
// Assume GreenRectangle inherits Rectangle and
// potentially has its own Render()
std::vector<std::unique_ptr<Rectangle>> components;
components.push_back(std::make_unique<Rectangle>(...));
components.push_back(
std::make_unique<GreenRectangle>(...)
);
for (const auto& ptr : components) {
ptr->Render(surface); // Which Render() is called?
}
Here, ptr
is always of type std::unique_ptr<Rectangle>
, giving access via a Rectangle*
. At compile time, the compiler only knows it's calling Render
through a Rectangle
pointer. How does it know whether to call the original Rectangle::Render
or the specific GreenRectangle::Render
for the second element?
virtual
This is where virtual
comes in. When you declare a function as virtual
in a base class, you tell the compiler: "The decision of which version of this function to call should be made at runtime, based on the actual type of the object being pointed to, not just the type of the pointer." This is dynamic dispatch or late binding.
// Rectangle.h
#pragma once
#include <SDL.h>
#include <iostream> // For std::cout example
class Rectangle {
// ... other members ...
public:
Rectangle(const SDL_Rect& Rect) : Rect{Rect} {}
// Declare Render as virtual
virtual void Render(SDL_Surface* Surface) const {
auto [r, g, b, a]{
isPointerHovering ? HoverColor : Color
};
SDL_FillRect(
Surface, &Rect,
SDL_MapRGB(Surface->format, r, g, b)
);
}
// Declare HandleEvent as virtual
virtual void HandleEvent(SDL_Event& E) {
if (E.type == SDL_MOUSEMOTION) {
isPointerHovering = isWithinRect(
E.motion.x, E.motion.y
);
} else if (
E.type == SDL_WINDOWEVENT &&
E.window.event == SDL_WINDOWEVENT_LEAVE
) {
isPointerHovering = false;
} else if (E.type == SDL_MOUSEBUTTONDOWN) {
if (isPointerHovering &&
E.button.button == SDL_BUTTON_LEFT
) {
std::cout << "Rectangle clicked!\\n";
}
}
}
// IMPORTANT: Base classes with virtual functions
// should almost always have a virtual destructor
virtual ~Rectangle() = default;
// ... private members, isWithinRect ...
private:
SDL_Rect Rect;
SDL_Color Color{255, 0, 0};
SDL_Color HoverColor{0, 0, 255};
bool isPointerHovering{false};
// ... isWithinRect implementation ...
};
// GreenRectangle.h
#pragma once
#include "Rectangle.h"
class GreenRectangle : public Rectangle {
public:
GreenRectangle(const SDL_Rect& Rect)
: Rectangle{Rect} {
SetColor({0, 255, 0});
SetHoverColor({0, 180, 0});
}
// Override the virtual function (optional 'override' keyword helps)
// No need to repeat 'virtual' here, but good practice
// to add 'override' for clarity and compiler checks.
void HandleEvent(SDL_Event& E) override {
// Call the base class version first, if desired
// Rectangle::HandleEvent(E);
// Or provide completely new behavior
if (E.type == SDL_MOUSEMOTION) {
// Maybe GreenRectangles don't change color on hover
// isPointerHovering = isWithinRect(...); // Base logic
} else if (E.type == SDL_MOUSEBUTTONDOWN) {
// Check isPointerHovering (inherited private member access
// would need protected or accessor function in base)
// For simplicity, let's assume we can check hover state
// if (isPointerHovering && ...) {
// Direct call needs isWithinRect to be public/protected
if (isWithinRect(E.button.x, E.button.y) &&
E.button.button == SDL_BUTTON_LEFT) {
std::cout << "GreenRectangle clicked!\\n";
}
}
// Let base class handle other events if needed
// (This shows a mix; often you fully override or call base)
}
// Destructor is implicitly virtual because base is virtual
// ~GreenRectangle() override = default; // Optional but clear
};
Now, when the loop calls ptr->Render(surface)
or ptr->HandleEvent(E)
, the program checks the actual object type at runtime. If ptr
points to a GreenRectangle
, GreenRectangle::HandleEvent
will be executed (if overridden). If it points to a base Rectangle
, Rectangle::HandleEvent
runs.
In our SDL UI structure using std::vector<std::unique_ptr<Rectangle>>
, we need virtual
functions for Render
and HandleEvent
. Without virtual
, the loop calling ptr->Render()
would always call Rectangle::Render
, even if the pointer actually pointed to a GreenRectangle
object. The specific behaviors or appearances defined in GreenRectangle
would be ignored when accessed through the base pointer.
Crucially, if a class has any virtual
functions, it should almost always have a virtual
destructor, even if it's empty or defaulted (virtual ~Rectangle() = default;
).
When you delete
a derived object through a base class pointer (which std::unique_ptr
does automatically when it goes out of scope or is reset), having a virtual destructor ensures that the derived class's destructor is called first, followed by the base class's destructor.
Without it, only the base destructor might run, potentially leading to resource leaks if the derived class managed resources.
Answers to questions are automatically generated and may not have been reviewed.
Discover how to organize SDL components using manager classes, inheritance, and polymorphism for cleaner code.