When we forward-declare a class, like we did with class UI;
in Button.h
, we're telling the compiler, "Trust me, there's a class named UI
defined somewhere else."
// Button.h
#pragma once
#include <SDL.h>
#include "Rectangle.h"
class UI; // Forward declaration
class Button : public Rectangle {
public:
// ...
// We can USE the name UI here:
Button(UI& UIManager, const SDL_Rect& Rect);
private:
// We can declare members of this type:
UI& UIManager;
};
At this point, inside Button.h
, UI
is considered an incomplete type. The compiler knows the name UI
refers to a class type, but it doesn't know anything else about it:
UI
is (its size in memory).UI
has.UI
has.Because the compiler lacks detailed information, there are limitations on what you can do with an incomplete type directly within the header file that only sees the forward declaration:
UI
(e.g., UI myUI;
). The compiler needs to know the size.sizeof(UI)
.UI
(e.g., UIManager.someMember
). The compiler doesn't know if someMember
exists.UI
(e.g., UIManager.SomeFunction()
). The compiler doesn't know if SomeFunction
exists or what its signature is.UI
.dynamic_cast
.However, you can do things that don't require knowing the class's internal details:
UI
(e.g., UI* ptr;
or UI& ref;
). Pointers and references usually have a fixed size regardless of the type they point/refer to.UI
(e.g., void ProcessUI(UI* uiPtr);
or UI& GetUIManager();
).In our lesson, the Button
class needed to call the SetRectangleColors()
method on its UIManager
 reference.
// Button.h - Trying to DEFINE OnLeftClick here
// ...
class Button : public Rectangle {
public:
Button(UI& UIManager, const SDL_Rect& Rect);
// Attempting definition in the header:
void OnLeftClick() override {
// ERROR! UI is an incomplete type here!
UIManager.SetRectangleColors({0,255,0,255});
}
private:
UI& UIManager;
};
This fails because when the compiler processes Button.h
, it only sees class UI;
. It has no idea if UI
actually has a method called SetRectangleColors
or what arguments it takes.
.cpp
The solution is to only declare the function in the header and define it in a corresponding source file (.cpp
).
// Button.h - Declaration Only
// ...
class Button : public Rectangle {
public:
Button(UI& UIManager, const SDL_Rect& Rect);
void OnLeftClick() override; // Declaration
private:
UI& UIManager;
};
// Button.cpp - Definition
#include "Button.h"
#include "UI.h" // Include the full definition!
void Button::OnLeftClick() {
// OK! Compiler now sees the full definition
// of UI from UI.h and knows about the method.
UIManager.SetRectangleColors(
{0, 255, 0, 255} // Green
);
}
In Button.cpp
, we #include "UI.h"
. This gives the compiler the full definition of the UI
class before it compiles the Button::OnLeftClick
method. Now, UI
is a complete type, and the compiler can verify that SetRectangleColors
exists and is being called correctly.
Forward declarations and incomplete types are essential tools for breaking circular dependencies between headers and reducing compilation times, but they require us to separate declarations (in .h
) from definitions that require complete types (in .cpp
).
Answers to questions are automatically generated and may not have been reviewed.
Learn to create interactive buttons in SDL2 and manage communication between different UI components.