Summary
C++ supports object-oriented programming through classes, inheritance, and virtual functions. Unlike C#, where virtual dispatch is the default for methods (and can be suppressed with sealed), C++ methods are non-virtual by default — you must explicitly declare them virtual to enable runtime polymorphism. This page covers the class model from Stroustrup’s A Tour of C++ Chapters 2 and 5, with notes on differences from C#.
(Stroustrup, A Tour of C++, 3rd ed., see source-a-tour-of-cpp)
struct and class
In C++, struct and class are the same feature with one difference:
struct | class | |
|---|---|---|
| Default member access | public | private |
| Everything else | Identical | Identical |
A struct can have constructors, member functions, inheritance, and virtual functions exactly as a class can. The convention is to use struct for simple data aggregates and class for types with significant invariants and a deliberate interface.
struct Vector {
double* elem; // public by default
int sz;
};
class Vector {
public:
Vector(int s);
double& operator[](int i);
int size() const;
private:
double* elem; // private
int sz;
};C# comparison: C# struct is a value type (stack-allocated, copied on assignment); C++ struct is identical to class and not a value type.
Constructors
A constructor is a member function with the same name as the class. It is guaranteed to run when an object is created:
class Vector {
public:
Vector(int s) : elem{new double[s]}, sz{s} {} // member initialiser list
~Vector() { delete[] elem; } // destructor
// ...
};Member initialiser list: the : elem{...}, sz{s} syntax initialises members before the constructor body runs. This is required for const members, references, and base class constructors.
Default constructor: a constructor that takes no arguments. Defining one prevents uninitialized objects of that type:
complex() : re{0}, im{0} {} // default: {0, 0}explicit single-argument constructors: prevent implicit conversion from the argument type. Nearly always the right choice:
class Vector {
public:
explicit Vector(int s); // Vector v = 7; is now an error
};const member functions
A member function that does not modify the object should be marked const. This allows it to be called on const objects and communicates intent:
class Vector {
public:
int size() const { return sz; } // can be called on const Vector
double& operator[](int i); // non-const: returns modifiable reference
const double& operator[](int i) const; // const version
};Concrete types
A concrete type behaves like a built-in type: its representation is part of its definition, it can be stack-allocated, copied, and moved. Examples: int, double, std::string, std::vector<T>.
Key properties:
- Can be placed on the stack or in other objects (no heap required)
- Can be copied and moved
- Destructor releases resources (see cpp-memory-management)
Vector v(6); // stack-allocated Vector, destroyed at end of scopeAbstract types and virtual functions
An abstract type defines an interface without exposing its representation. It is accessed through a pointer or reference (because the concrete type is not known at the point of use) and provides at least one pure virtual function:
class Shape {
public:
virtual void draw() const = 0; // pure virtual: must be overridden
virtual void rotate(int angle) = 0;
virtual ~Shape() {} // virtual destructor: required for correct deletion
};A class with any pure virtual function cannot be instantiated directly. Derived classes that implement all pure virtual functions are concrete:
class Circle : public Shape {
public:
Circle(Point p, int r) : center{p}, radius{r} {}
void draw() const override { /* ... */ }
void rotate(int angle) override { /* ... */ }
private:
Point center;
int radius;
};override: explicitly marks a function as overriding a virtual base function. The compiler checks that a matching virtual function exists — catches typos in function signatures.
final: marks a class or virtual function as not further overridable.
Virtual destructor: if a class has virtual functions, its destructor should also be virtual. Without it, deleting a derived object through a base pointer calls only the base destructor — leaking the derived object’s resources.
Class hierarchies
C++ supports single and multiple inheritance. A derived class is-a base class:
class Shape { /* ... */ };
class Circle : public Shape { // Circle is-a Shape
Point center;
int radius;
public:
void draw() const override;
};
class Smiley : public Circle { // Smiley is-a Circle
vector<unique_ptr<Shape>> eyes;
unique_ptr<Shape> mouth;
public:
void draw() const override;
};public inheritance means the base class’s public interface is part of the derived class’s public interface (is-a relationship). Private and protected inheritance express different semantic relationships and are used rarely.
Polymorphic dispatch through pointer/reference:
void draw_shape(const Shape& s) {
s.draw(); // calls Circle::draw() or Smiley::draw() at runtime
}
Shape* p = new Circle{center, radius};
p->draw(); // calls Circle::draw() through vtblThe vtbl (virtual function table) is a compiler-generated table of function pointers, one per class with virtual functions. Each virtual call does one additional indirection through the vtbl — a small constant cost.
C# comparison: in C#, all non-static methods are virtual by default (overridable). In C++, no functions are virtual by default. You must explicitly write virtual to enable polymorphic dispatch. This is a common source of bugs when porting from C#: overriding a non-virtual function in C++ silently hides it rather than dispatching polymorphically.
enum class
C++ provides two enumeration forms:
enum class Color { red, blue, green }; // scoped: Color::red
enum Color { red, blue, green }; // plain: red (pollutes surrounding scope)enum class (scoped enum):
- Enumerators are scoped:
Color::red, notred - Does not implicitly convert to
int - Cannot mix values from different
enum classtypes
Color col = Color::red; // OK
int i = Color::red; // ERROR: no implicit conversion
Color c = 2; // ERROR: no implicit conversion
int x = int(Color::red); // OK: explicit conversion
Color y {5}; // OK: explicit from underlying typePlain enum exists for compatibility with C and older code. Prefer enum class in new C++ code.
C# comparison: C# enums are closest to C++ plain enums but are implicitly int-backed and do require an explicit cast to convert to int. C++ enum class is stricter.
Operator overloading
C++ allows defining operators for user-defined types. This makes user types behave syntactically like built-in types:
complex operator+(complex a, complex b) { return a += b; }
bool operator==(complex a, complex b) {
return a.real() == b.real() && a.imag() == b.imag();
}The rule of conventional semantics: define operators with the meaning the user expects. + should add, == should test equality and imply != means !(a==b).
C++20’s spaceship operator <=> generates all six comparison operators from one definition:
auto operator<=>(const MyType&) const = default; // generates ==, !=, <, <=, >, >=C# comparison: C# also supports operator overloading with similar syntax. One difference: C# requires == and != to be defined in pairs; C++ has the spaceship operator to do all six at once.
In practice (Unreal Engine context)
In Unreal Engine (C++):
UCLASS,USTRUCT,UENUMmacros wrap C++ classes for Unreal’s reflection systemUObjectderived classes useNewObject<T>()rather thannew T()— Unreal manages their lifetime through its own garbage collector (distinct from C++ RAII)- Plain C++ classes and structs (not derived from
UObject) follow normal C++ RAII rules - Virtual functions are used heavily in
AActor,UActorComponent, etc. — the is-a hierarchy mirrors standard C++ class hierarchies UPROPERTY()andUFUNCTION()macros expose class members to Unreal’s Blueprint system
Understanding the difference between UObject-managed objects and plain C++ objects is critical for correct Unreal development.
Open questions
- Multiple inheritance in C++ is not supported in C# (except for interfaces). Unreal’s component model largely avoids deep multiple inheritance through composition. When is multiple inheritance actually used in game engine code?
- The vtbl cost is one pointer indirection per virtual call. For performance-critical game code (physics, AI), is virtual dispatch a bottleneck? The data-oriented design (DOD) and Entity-Component-System patterns partly exist to avoid it.
Related
- cpp-basics — fundamental types, pointers, references, scope
- cpp-memory-management — RAII, destructors, smart pointers (unique_ptr in class hierarchies)
- cpp-templates — parameterized types; templates as an alternative to inheritance for polymorphism
- data-oriented-design — an important counterweight to deep object hierarchies in engine code
- csharp-oop-fundamentals — C# OOP for comparison
- csharp-inheritance — C# inheritance; virtual by default contrast
- csharp-enums — C# enum comparison
- source-a-tour-of-cpp