Summary

C++ is a compiled, statically typed language in which every entity’s type must be known to the compiler. Its distinguishing characteristic for programmers coming from C# or Java is value semantics: assignment copies values by default; sharing an object requires explicit pointers or references. This page covers the language fundamentals through Chapter 1 of Stroustrup’s A Tour of C++ (3rd ed., C++20), with notes on how they differ from C#.

(Stroustrup, A Tour of C++, 3rd ed., see source-a-tour-of-cpp)


Compilation model

C++ programs are compiled to native machine code by a compiler (e.g., MSVC, Clang, GCC) and linked into an executable for a specific platform. The resulting binary is not portable between platforms — portability means the source code compiles on multiple targets, not that a single binary runs everywhere.

This contrasts with C# and Java, where code compiles to an intermediate representation (IL / bytecode) and runs on a virtual machine. C++ is faster to start up and closer to hardware, but has no garbage collector and no runtime type-checking safety net by default.


Fundamental types

C++ provides a small set of hardware-mapped types. Their sizes are implementation-defined (not fixed by the language) but reflect machine capabilities:

TypeMeaningTypical size
boolBoolean: true or false1 byte
charCharacter (or small integer)1 byte
intInteger4 bytes
doubleDouble-precision floating-point8 bytes
unsignedNon-negative integer; use for bitwise operations4 bytes

For fixed-width types, use standard aliases such as int32_t, uint64_t (from <cstdint>).

C# comparison: C# has int (always 32 bits), double, bool, char (UTF-16, 2 bytes). C#‘s types are specified by the language standard; C++‘s are specified by the hardware.


Initialisation

C++ offers multiple initialisation syntaxes:

double d1 = 2.3;       // traditional C-style
double d2 {2.3};        // braced-init (preferred)
double d3 = {2.3};     // also valid
 
int i1 = 7.8;            // i1 becomes 7 — silently truncates!
int i2 {7.8};             // ERROR: narrowing conversion rejected

The braced {} form prevents narrowing conversions — accidental implicit truncation. Prefer it for declarations where the type is named.

auto type deduction: when the type is obvious from the initialiser, use auto:

auto b = true;      // bool
auto i = 123;        // int
auto d = 1.2;        // double
auto z = sqrt(y);   // whatever sqrt() returns

Use auto to avoid repetition; be explicit where the type matters for correctness (e.g., double vs float precision).


Scope and lifetime

A declaration introduces a name into a scope. The three main scopes:

  • Local scope: inside a function or block { } — destroyed at the closing brace
  • Class scope: members of a class — destroyed when the object is destroyed
  • Namespace scope: declared in a namespace outside any function — destroyed at programme end
void f(int arg)        // arg: local scope
{
    string s {"Hello"}; // s: local scope
    // s is destroyed here when f() returns
}

Objects are constructed when they are declared and destroyed when they go out of scope. This deterministic lifetime is the foundation of RAII.

C# comparison: C# class instances live until the garbage collector claims them. C++ objects on the stack live until scope exit — predictable and deterministic.


const and constexpr

Two distinct notions of immutability:

KeywordMeaningWhen evaluated
const”I promise not to change this”Run time (value may depend on runtime input)
constexpr”Evaluate this at compile time”Compile time (value must be a constant expression)
constexpr int dmv = 17;          // compile-time constant
const double sqv = sqrt(var);    // run-time constant: computed once, then immutable
 
constexpr double square(double x) { return x*x; }  // constexpr function
constexpr double s = 1.4 * square(17);               // OK: compile-time
const double s2 = 1.4 * square(var);                 // OK: run-time

A constexpr function can be called with non-constant arguments but then produces a non-constant result.

C# comparison: C# const is compile-time only (like C++ constexpr). C#‘s readonly is closest to C++ const (set once, not necessarily at compile time).


Pointers

A pointer holds the memory address of another object:

char v[6];          // array of 6 chars
char* p = &v[3];   // p points to v's fourth element
char x = *p;        // dereference: x = value at address p

Key pointer operators in declarations:

  • T* — pointer to T
  • &var — address of var
  • *ptr — value at address ptr (dereference)

The null pointer: use nullptr (not 0 or NULL):

double* pd = nullptr;   // pd points to nothing
if (pd)                  // same as pd != nullptr
    // ...

Dereferencing a null or dangling pointer is undefined behaviour — the programme may crash or silently corrupt memory.

Pointer arithmetic is valid for arrays: p++ advances to the next element. This is how the range-for loop is implemented internally.


References

A reference is an alias for an existing object:

int x = 2;
int& r = x;   // r is bound to x
r = 7;         // modifies x through r; x is now 7

Key differences from pointers:

PropertyPointer (T*)Reference (T&)
Can be nullYes (nullptr)No — must bind to a valid object
Can be reboundYesNo — binding is permanent
Requires dereferenceYes (*p)No — implicitly dereferenced
Can do arithmeticYesNo

Const reference is the standard way to pass large objects to functions without copying:

void sort(vector<double>& v);              // pass by reference: sorts v in-place
double sum(const vector<double>& v);      // pass by const ref: reads but cannot modify

C# comparison: C# references are always non-nullable (unless nullable reference types are enabled) and are automatically dereferenced. C++ makes the null/non-null distinction explicit via the type system.


Value semantics

C++ has value semantics by default: assignment copies:

int x = 2;
int y = x;   // y is a copy of x; modifying y does not affect x

This is true for all types, including user-defined ones. Sharing requires explicit pointers or references.

C# comparison: in C#, class instances have reference semantics — assignment copies the reference, not the object. Two variables can point to the same object. In C++, assignment copies the object; you must use a pointer or reference to share.


Range-for loop

C++ range-for iterates over any sequence:

int v[] = {0, 1, 2, 3, 4, 5};
 
for (auto x : v)       // copy each element
    cout << x << '\n';
 
for (auto& x : v)      // reference: can modify
    ++x;               // increments each element

The range-for works with any type that provides begin() and end() — standard containers, arrays, strings, and any user-defined type implementing those functions.


In practice

  • Use {} for initialisation to catch narrowing conversions at compile time.
  • Use auto for type deduction where the type is obvious from context.
  • Use const on any variable or parameter that should not change — it documents intent and enables compiler optimisations.
  • Use constexpr for values needed at compile time (array sizes, template arguments, performance-critical constants).
  • Prefer nullptr to 0 or NULL.
  • Pass large objects by const reference to avoid copies.
  • Minimise pointer use in application code; prefer references where the object is guaranteed to exist.

Open questions

  • Stroustrup’s advice to prefer references over pointers assumes object lifetimes are clear. In game engine code with complex ownership (entities, components, pools), lifetime management is often subtle. Where does the reference/pointer choice matter most in Unreal Engine?
  • C++20 modules (import/export) reduce compile-time overhead dramatically but are not yet universally adopted. Unreal Engine uses headers. How does this affect Unreal-specific C++ patterns?