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:

structclass
Default member accesspublicprivate
Everything elseIdenticalIdentical

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 scope

Abstract 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 vtbl

The 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, not red
  • Does not implicitly convert to int
  • Cannot mix values from different enum class types
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 type

Plain 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, UENUM macros wrap C++ classes for Unreal’s reflection system
  • UObject derived classes use NewObject<T>() rather than new 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() and UFUNCTION() 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.