1 of 121

ECS7014P – Advanced Game Development

C++

Lecturer: Diego Perez Liebana

School of Electronic Engineering and Computer Science

2 of 121

C++

Overview

3 of 121

Introduction

C++ is… a compiled language:

  • A compiler needs to be executed to transform source code into an executable (machine code).

In fact, there’s a step in between these two, run by the linker.

3

Source file 1

Source file 2

Source file 3

Object file 1

Object file 2

Object file 3

Executable file

(portable)�.h/.hpp, .cpp, …

(not portable) .o/.obj files

(not portable)

.exe / (bin) file

Compiler

Linker

Library 1

Library 2

Library 3

.lib / .dll

Compilation and Preprocessing errors?

Linking errors?

Search in Games

4 of 121

Introduction

C++ is… a federation of languages:

  • C: statements, pre-processor, built-in data types, arrays, pointers…
  • Object-Oriented C++: classes, encapsulation, inheritance, polymorphism, virtual functions…
  • Template C++: generic programming, template metaprogramming (TMP)
  • The Standard Template Library (STL): algorithms (sorting, searching), containers (vector, map, list, queue), iterators…
  • Rules may vary depending on the part of C++ you’re using!

C++ is…

  • Complex. You don’t need to know everything about C++ to use it. Excellence comes with experience.
  • Fast. That’s why high-performance applications (i.e. games!) use it. Even if you don’t use C++ directly, chances are the underlying engine is written in C or C++.
  • Statically typed: The type of every entity (object, value, name, expression) must be known by the compiler at the time of its use.

4

Search in Games

5 of 121

A note on Compilers and Versions

There are multiple C++ compilers. The ones used more often are:

  • Microsoft Visual C++ compiler
  • The GNU Compiler Collection (GCC/LLMV)
  • Clang

Also, C++ has different versions (C++2.0, C++ 98, C++03, C++11, C++14, C++17, C++20)

  • The latest versions of most compilers support all core C++17 features, and some C++20.

And there exist multiple Integrated Development Environments (IDE) that support programming in C++:

  • Microsoft Visual Studio, Clion, Dev-C++, Xcode and Visual Studio Code (Mac), and more…

5

Search in Games

6 of 121

A note on Compilers and Versions

During this course, we will use:

  • The C++14 language level.
  • Microsoft Visual C++ compiler (MSVC 14.27).
  • Microsoft Visual Studio 2022 (Community Edition).
  • Windows.
  • You are strongly recommended to use this setting (which is installed in the labs) or a later versions of the different software. We won’t be able to provide much support for different OS, IDEs or compilers.

More information:

6

Search in Games

7 of 121

Dissecting Hello World!

7

Classic minimal C++ program:

// This program outputs the message “Hello, World!” to the monitor

#include <iostream>

int main() //C++ programs start by executing the function main()

{

std::cout << “Hello, World!\n”; //output “Hello, World!”

return 0;

}

\n is a special character to denote a new line

cout refers to a standard output stream

This is a comment, written after the double slash token //

This code prints the output “Hello, World” to console.

In C++, string literals are delimited by double quotes “”

Search in Games

8 of 121

Dissecting Hello World!

Classic minimal C++ program:

8

// This program outputs the message “Hello, World!” to the monitor

#include <iostream>

int main() //C++ programs start by executing the function main()

{

std::cout << “Hello, World!\n”; //output “Hello, World!”

return 0;

}

This is a header file (.h/.hpp), which is the type of file that follows an #include directive.

A header file contains definitions of terms (such as cout).

All C++ programs start executing from the function main.

Every C++ must have a function called main.

The #include directive indicates the computer to make the facilities in the <iostream> library available.

For instance, the standard output stream (std::cout) in this code.

This code prints the output “Hello, World” to console.

A function is a named sequence of instructions with 4 parts:

int main()

{

// ...

return 0;

}

A return type, indicating the result of the sequence (if any). Here, the type is int.

A name. Here: main.

A parameter list, between parenthesis. Here, it is empty.

A function body, between the curly braces {}, which lists the instructions of the function.

Search in Games

9 of 121

C++

Declarations and Definitions

10 of 121

Declarations

A declaration is a statement that introduces a name into a scope:

  • Specifies a type for what is named.
  • Optionally, it specifies an initializer.

Before a name can be used in a C++ program, it must be declared.

10

int a = 7; //int variable

const double cd = 8.7; //double precision floating-point constant

double sqrt(double); //function taking a double argument returning a double

vector<Token> v; //a vector of Token variables

int main()

{

std::cout << f(i) << ‘\n’;

}

cout, f(), i need to be declared, or the compiler will throw ‘undeclared identifier’ errors.

#include <iostream> //the declaration of cout is here

int main()

{

std::cout << f(i) << ‘\n’; //f() and i are still undefined

}

Search in Games

11 of 121

Declarations

Most declarations are found in header files (iostream is one), which indicate the interface (how is something meant to be used and called). By including #include <iostream> we are telling the compiler where to find these declarations.

Names can also be declared in the same file:

  • Every definition is a declaration
  • Not every declaration is a definition

11

#include <iostream> //declaration of cout (through include)

int f(int); //declaration f(int)

int main()

{

int i = 7; //declaration of i

std::cout << f(i); //all is declared

return 0;

}

This code compiles!

but it will not link!

There is no definition of what f() does

(i.e. there’s no body defined for this function)

#include <iostream>

int f(int){ //declaration and

return 0; //definition of f(int)

}

int main()

{

int i = 7; //declaration and definition

std::cout << f(i); //compiles and links!

return 0;

}

Search in Games

12 of 121

Definitions

A definition specifies exactly what a name refers to.

  • Names can’t be defined twice

  • Names can be declared twice, as long as consistency is kept:

  • Definitions are stored in (consume) memory, while declarations don’t.

12

double sqrt(double d) {/* ... */} //definition

double sqrt(double d) {/* ... */} //error: double definition

int var; //definition

int var; //error: double definition

double sqrt(double d); //declaration

double sqrt(double d); //declared again, all good.

int sqrt(double d); //declared again, error!

There cannot be two functions with the same name (sqrt), taking the same argument list and returning a different type.

The compiler can’t infer which one to call!

Search in Games

13 of 121

Declarations and Definitions

Normally, declarations are listed in header files.

These files can be referenced to by using the #include directive.

13

#include <iostream>

#include “vector.h”

void printHello(){

std::cout << “Hello!” << std::endl;

}

Vector::Vector(int s) : elem{new double[s]}, sz{s} {

}

int Vector::size(){

return sz;

}

void printHello();

class Vector {

public:

Vector(int s);

int size();

private:

int sz;

double* elem;

};

vector.h

Header file (declares the interface)

vector.cpp

Source file (defines the implementation)

For member functions, the class scope needs to be included.

Search in Games

14 of 121

Functions

A function is a named sequence of instructions. It cannot be called unless it has been declared.

A function declaration needs to provide, at least, the name of the function, the return type and the parameters. Examples:

14

Elem* next_elem(); //no arguments, returns a pointer to Elem (Elem*).

void exit(int val); //int argument, returns nothing.

double sqrt(double); //double argument, returns double.

Related to this…

Note not only functions can be declared; also:

extern int x; //object/type declaration.

std::size_t numDigits(int number); //function declaration.

class Widget; //class declaration.

template<typename T> class GraphNode; //template declaration.

A declaration tells compilers about the name and type of something, omitting certain details.

Semicolon needed at the end.

Argument name not needed in declarations (with no definition)

Search in Games

15 of 121

Functions

The function declaration reveals its signature or type:

Functions can be members of a class, (i.e. they are member functions), and in that case the name of the class it’s also part of the type:

Function types and names are required by the compiler to choose the most appropriate function to invoke for each call.

15

double get(const vector<double>& vec, int index); //type is: double(const vector<double>&, int)

char& String::operator[](int index); //type is: char& String::(int)

void print(int); //takes an integer argument

void print(double); //takes a double argument

void print(string); //takes a string argument

void user(){

print(42); //calls print(int)

print(9.65); //calls print(double)

print(“Paris”); //calls print(string)

}

Multiple functions with same name is known as function overloading.

Search in Games

16 of 121

Functions

A function definition provides compilers with the details a declaration omits:

16

int x; //object/type definition.

std::size_t numDigits(int number) //function definition.

{

std::size_t digitsSoFar = 1;

while ((number /= 10) != 10) ++digitsSoFar;

return digitsSoFar;

}

class Widget { //class definition.

public:

Widget(); //This is a class constructor

~Widget(); //This is a class destructor

}

template<typename T> //template definition.

class GraphNode{

GraphNode();

~GraphNode();

}

Same as its declaration!

Body of the function.

Lists members of the class.

Lists members of the template.

Search in Games

17 of 121

C++

Types I: basic types

18 of 121

Types and Variables: definition

All names and expressions have a type (i.e. functions).

Some useful definitions:

  • A type defines a set of possible values and operations for an object.
  • An object is some memory that holds a value of some type.
  • A value is a set of bits interpreted according to a type.
  • A variable is a named object.

There’s a myriad of fundamental types defined in C++:

https://en.cppreference.com/w/cpp/language/types

Examples: short, int, short int, long, double, unsigned int, long long, bool, char, signed char, float…

18

int inch; //type is int. “inch” is an integer variable.

Search in Games

19 of 121

Types and Variables

There are multiple fundamental data types. Here are some:

19

Type

Size

Category

Meaning

Example

float�double�long double

4 bytes

8 bytes

8 bytes

Floating Point

a number with a fractional part

3.14159

bool

1 byte

Integral (Boolean)

true or false

true

char�wchar_t�char8_t (C++20)�char16_t (C++11)�char32_t (C++11)

1 byte

1 byte

1 byte

2 bytes

4 bytes

Integral (Character)

a single character of text

‘c’

short�int�long�long long (C++11)

2 bytes

4 bytes

4 bytes

8 bytes

Integral (Integer)

positive and negative whole numbers, including 0

64

std::nullptr_t (C++11)

N/A

Null Pointer

a null pointer

nullptr

void

N/A

Void

no type

n/a

Types can be made unsigned:

All numeric types are “signed” by default.

C++ doesn’t have a fundamental “string” data type. Instead, we use the compound data type from the standard library:

unsigned int v; //0 .. 4.2M.

signed int v; //-2.1M .. 2.1M

#include <string> //allows using std::string

std::string myName(“Peter”);

Search in Games

20 of 121

Types and Variables: arrays

An array allows to hold a contiguously allocated sequence of elements of the same type in memory.

  • The size of the array needs to be a constant expression.
  • In a declaration, [] means “array of”.
  • In an expression, you can access array elements with the [] operator:
    • Indexing outside the range of the array can lead to undefined behaviour.

Arrays can be initialized with an initializers list:

20

char v[6]; //array of 6 characters, stored in the stack.

int primes[5]{2, 3, 5, 7, 11}; //5 elements, all initialized

int evens[5]{2, 4, 6}; //5 elements, only the first 3 initialized

int array[5]{}; //5 elements, all zero-initialized.

int odds[]{1, 3, 5, 7, 9}; //5 elements, size determined by initializers list.

int arrayLength = std::size(odds); //std::size provides the length of the array.

char firstLetter = v[0];

Search in Games

21 of 121

Types and Variables: looping

One of the most common ways of iterating through the elements of an array is a for loop.

21

for (int i = 0; i < 6; ++i) //iterates through all elements.

v[i] = ‘a’;

for (auto i = 0; i < std::size(v); ++i) //iterates through all elements (auto).

for (int i = 0; i < std::size(v); ++i) //iterates through all elements, using std::size().

for (auto x : v) //iterates through all values (no index).

//this COPIES the value of v[i] to the variable x

for (auto& x : v) //iterates through all values (no index).

//here x is a REFERENCE to v[i]

Changing x doesn’t change the value in v

Changing x changes the value in v

Search in Games

22 of 121

Types and variables: initialization

There are 4 basic ways to initialize (give a value) to a variable in C++:

  1. No initialization:

Using variables that have not been initialized may lead to undefined behaviour. This may lead to inconsistencies or even the program to crash. Therefore, always initialize variables yourself!

  1. Copy initialization: initializing via an equals “=“ sign.

This expression copies the value on the right-hand side to the value that is created on the left hand side.

This is efficient only for simple data types (int, float, char, etc.), but no for complex ones (i.e. classes).

22

int inch; // “inch” has no value. By default, in will take some unused memory value

// Some compilers will give you an error if you try to use this. But not all!

int inch = 5; // “inch” takes the value “5”

Search in Games

23 of 121

Types and variables: initialization

  1. Direct initialization: using parenthesis ()

For simple data types (int, float, char), this is the same as copy initialization. For complex types, like classes, direct initialization tends to be more efficient than copy initialization.

  1. Brace initialization: using braces {}

Direct brace and copy brace are almost identical, with “direct brace” being normally the preferred one (simpler).

Brace initialization will throw compilation errors if the type on the right-hand side is incompatible with the variable type. Therefore, it’s recommended that you use direct brace initialization.

Value initialization will initialize the variable to zero (or empty). This is better than no initialization as it avoids causing undefined behaviour!

23

int inch (5); // “inch” takes the value “5”

int inch1 {5}; // direct brace initialization

int inch2 = {6}; // copy brace initialization

int inch3 (); // value initialization

Search in Games

24 of 121

Types and Variables: casting

C++ is a typed language that allows casting, which consists of converting one value from one type to another (type conversion). There are different ways of doing castings in C++:

  • Implicit type conversion: This is an old-style cast and it is not recommended.

C++ Style casts:

  • Const cast: typically used to cast away the const-ness of an object:

  • Dynamic cast: for safe down-casting, from a base class to a derived one. Computationally expensive!

  • Reinterpret cast: for low-level casts that are implementation-dependent (unportable) results.

24

int inches {10};

long inchesL = (long) inches; //type is now long.

const int inches {10};

int noConstInches = const_cast<int>(inches); //type is no longer const.

BaseClass* obj1 = new DerivedClass(10);

DerivedClass* obj1Der = dynamic_cast<DerivedClass*>(obj1); //type is a derived class object.

Search in Games

25 of 121

Types and Variables: casting

  • Static cast: can be used to force implicit conversions:
    • non-const to const, between fundamental types, void* to type*, etc.
    • recommended instead of implicit cast conversions.

25

int inches {10};

long inchesL = static_cast<long>(inches); //type is now long

int nonConstWeight {10};

const int constWeight = static_cast<const int>(nonConstWeight);

Avoid casts whenever practical, especially dynamic_cast in performance sensitive code.

If casting is necessary, try to hide it inside a function (so caller doesn’t need to cast).

Prefer C++ style cast to old-style casts. They are easier to see and more specific about what they do.

Effective C++, Item 27: Minimize casting

Search in Games

26 of 121

C++

Types II: auto, scope and const

27 of 121

Types and Variables: auto

C++ can do type deduction. The type of a variable can be determined at compile time, and we don’t need to specify the type in the code. In that case, we can use the keyword auto as the type.

27

auto d {1.0}; //1.0 is a double literal, type is deduced to be a double

int add(int x, int y) {

return x + y;

}

auto sum = add(3, 5); //add() returns an int, so ‘sum’ is deduced to be an int.

std::vector<Character*> team;

for (auto c : team) //The vector ‘team’ has pointers to Character, so each

c->sayHi(); // element is deduced to be a Character*

auto variables must be initialized, so they are generally immune to type mismatches that can lead to portability or efficiency problems, can ease the process of refactoring and typically require less typing than explicitly specified types.

Effective Modern C++, Item 5: Prefer auto to explicit type declarations.

Search in Games

28 of 121

Types and Variables: auto

Advantages of using auto:

  • Convenience (less code to write, simpler to read).
  • auto requires initialization, so it avoids having uninitialized variables.
  • It requires no type conversion (it doesn’t impact performance).

Disadvantages:

  • It won’t warn you if the type is not the one that you intended to use.
  • auto& may bring problems as it drops the reference (see later on).

28

Invisible proxy types can cause auto to deduce the “wrong” type for an initializing expression.

The explicitly typed initializer idiom forces auto to deduce the type you want it to have:

Effective Modern C++, Item 6: Use the explicit casting to avoid undefined behaviour with type deduction

auto index = static_cast<int>(d * c.size());

Search in Games

29 of 121

Types and Variables: operators

Arithmetic and other operators:

Expressions are evaluated left to right.

Assignments are evaluated right to left, with the assignment (=) operator:

Operators can be overloaded:

29

x+y //plus

x-y //minus

x*y //multiply

x/y //divide

x%y //remainder/modulus

-x //unary minus

+x //unary plus

x==y //equal

x!=y //not equal

x<y //less than

x>y //greater than

x<=y //less than or equal

x>=y //greater than or equal

comparison

arithmetic

x&y //bitwise and

x|y //bitwise or

x^y //bitwise exclusive or

~x //bitwise complement

x&&y //logical and

x||y //logical or

!x //logical negation

logical

x+=y //x = x+y

x-=y //x = x-y

++x //x = x+1

x++ //x = x+1

--x //x = x-1

x-- //x = x-1

x*=y //x = x*y

x/=y //x = x/y

x%=y //x = x%y

arithmetic assignment

double val = d+i; //the result value of (d+1) is assigned to the variable ‘val’

Vector operator+(Vector a, Vector b) {

return Vector{a.x + b.x, a.y + b.y};

}

Vector one{1.0, 3.0};

Vector two{-2.0, 5.1};

Vector sum = one + two;

Search in Games

30 of 121

Types and Variables: scope

A declaration of an object introduces its name into a scope:

  • Local scope: A name declared in a function or lambda is called a local name. Its scope extends from its point of declaration to the end of the block (delimited by a { } pair) in which the declaration occurs.
  • Class scope: A name is a member name (or class member name) if defined in a class, outside any function, lambda or enum class. Its scope extends from the opening ({) to the end (}) of its enclosing declaration.
  • Namespace scope: A name is a namespace member name if defined in a namespace, outside any function, lambda, class or enum class. Its scope extends from the opening ({) to the end (}) of its enclosing declaration.
  • Global scope: A name not declared inside any construct is called a global name and is said to belong to the global namespace.

Objects must be initialized before they are used and will be destroyed at the end of their scope.

30

Search in Games

31 of 121

Types and Variables: scope

Examples:

31

namespace Math{

int w; //namespace scope: visible inside namespace Math

class MyVector{

vector<int> v; //class scope: visible inside class MyVector

public:

int largest(){

int r {0}; //local scope: visible inside function largest()

for(int i = 0; i<v.size(); ++i)

r = max(r, abs(v[i])); //local scope: i is in the for-loop statement’s scope

return r; // i no longer visible here

}

}

}

int x, y; //global scope: these are a global variables, avoid them!!

int f()

{

int x; //function scope: it hides global variable x. Try to avoid this too.

x = 7;

{

int x = y; //local scope ({} block): hides function’s x, initialized by global y

++x; //the x from the previous line.

}

return x; //the x from function f()’s first line

}

Search in Games

32 of 121

Types and Variables: scope

A switch statement:

32

switch (expression)

{

case value1:

{

// Doing something here

break;

}

case value2:

{

// Doing something else here

break;

}

default:

// Code for no case match

}

Expression that is evaluated once for this statement.

The type of the expression needs to be an integer or an enumerated type

(or a class type that can be converted to an integer or an enumerated type).

The expression is evaluated against the values of all defined cases.

Cases are defined between {} brackets. These brackets define the scope for the case.

The break statement (optional) jumps off the switch block.

Don’t do this (like in Java/C#)!

switch (expression)

{

// “case” are just labels,

// they don’t define scope.

case value1:

// Doing something here

break;

case value2:

// Doing something here

break;

}

The default block (optional) is run if no case is matched.

Search in Games

33 of 121

Types and Variables: const

C++ allows you to define the mutability of variables in their scope:

  • const is a modifier that tells the compiler that we are defining an object that is not supposed to change.

Note, however, that the value of a const can be computed at runtime:

33

const int cubeSides {6};

cubeSides = 10; //compilation error: can’t assign to a variable that is const

int f() { return 3+3; }

int main()

{

const int cubeSides = f(); //Perfectly valid

return 0;

}

Constants defined with the preprocessor directive #define may not get entered in the symbol table: compilation errors may be quite obscure.

#define ASPECT_RATIO 1.653 🡪 const double AspectRatio = 1.653;

Effective C++, Item 2: Always prefer const to #define

Search in Games

34 of 121

Types and Variables: const

In pointers:

34

const allows you to specify a semantic constraint: things that should not be ever changed.

It transforms violations of these constraints into compilation errors.

Effective C++, Item 3: Use const whenever possible

char greeting[] = “Hello”;

char *p = greeting; //non-const pointer

//non-const data

const char *p = greeting; //non-const pointer

//const data

char* const p = greeting; //const pointer

//non-const data

const char* const p = greeting; //const pointer

//const data

const Vector& features(int id); //returned vector is constant

Vector& features(const int id); //int id parameter is constant

Vector& features(int id) const; //function is constant, body will

//not modify any member variable

const Vector& features(const int id) const; //can be combined

Member functions:

Search in Games

35 of 121

Types and Variables: const

A constant function can give a compilation error if the const is violated:

The keyword mutable allows you to by-pass this restriction in non-static data members:

35

#include <iostream>

#include “vector.h”

Vector::Vector(int s) : elem{new double[s]}, sz{s} {

}

int Vector::size() const{

sz = 0; //This will give a compilation error

return sz;

}

class Vector {

public:

Vector(int s);

int size() const;

private:

int sz;

double* elem;

};

vector.h

vector.cpp

/* ... *//

int Vector::size() const{

sz = 0; //This is now okay.

return sz;

}

class Vector {

/* ... */

private:

mutable int sz;

};

vector.h

vector.cpp

Search in Games

36 of 121

Types and Variables: constexpr

constexpr represents an expression that must be evaluated at compile time.

  • All constexpr objects are const, but not all const objects are constexpr
  • In a constexpr function:
    • If arguments are known during compilation: result computed during compilation
    • If one or more values are not known, result will be computed during runtime

36

constexpr int cubeSides = 6; //the value of cubeSides is calculated by the compiler.

constexpr objects are const and are initialized with values known during compilation.

constexpr functions can produce compile-time results when called with arguments whose values are known during compilation.

Effective Modern C++, Item 15: Use constexpr whenever possible

class Point{

public:

constexpr Point(double xVal = 0, double yVal = 0) : x(xVal), y(yVal) {}

private:

double x, y;

}

//constructor of Point run during compilation

constexpr Point p1(9.4, 27.7);

Search in Games

37 of 121

C++

Utils

38 of 121

Input/Output

The Input/Output (I/O) stream library provides formatted and unformatted ways to read and write text and numeric values:

  • ostream converts typed objects to a stream of characters (bytes).
    • Defines output for every stream type.
    • Uses the operator “<<“ (put to).
    • cout is the standard output, cerr is the error output stream.
    • endl can be sent through “<<“ for a line end.
  • istream converts a stream of characters to typed objects.
    • Defines inputs for stream types with character representations.
    • Uses the operator “>>” (get from).
    • cin is the standard input stream.

Similar to standard input/output, other streams define I/O for strings and files:

  • <fstream>: ifstream (reading from file), ofstream (writing to file), fstream (read/write to/from file).
  • <sstream>: istringstream (reading from a string), ostringstream (writing to a string), stringstream (guess ☺).

38

#include <iostream>

int main()

{

int intVal = 5;

std::cout << “Text to output stream” << std::endl;

std::cout << intVal;

int intIn;

//Reads an integer into intInt

std::cin >> intInt;

}

Search in Games

39 of 121

File System

In most environments you’ll have a file system that grants access to permanent information stored in files.

The library <filesystem> (https://en.cppreference.com/w/cpp/filesystem) offers a uniform interface to most facilities of most file systems in C++ 17 or later.

39

#include <fstream>

#include <sstream>

// ...

// Simple example of a String stream:

std::stringstream ss;

std::string extension = ".txt";

ss << "Example" << extension;

//Extract std::string from the stream.

std::string filename = ss.str();

std::ifstream ifs;

ifs.open(filename); //Open a file

if (ifs.is_open()) //Check it open was successful.

{

std::string line;

// Read line by line using std::getline(...)

while (std::getline(ifs, line))

{

// Do something with line (here, print it):

std::cout << line << std::endl;

}

}

Search in Games

40 of 121

The Preprocessor

The preprocessor step is a executed before the compilation phase. The output of the preprocessor is a single file which is then passed to the actual compiler:

It’s possible to include preprocessor directives to control the behaviour of the preprocessor.

  • They take the form “#” + characters (with no spaces)
    • In fact, #include “Game.h” is a preprocessor directive that includes a header file into a cpp file.
  • They are not C++ statements (no need for a semicolon “;” at the end)

40

Source files

Object files

Executable file

Compiler

Linker

Libraries

.lib / .dll

Preprocessor

Compiler

Search in Games

41 of 121

The Preprocessor Directives

Apart from #include, other common preprocessor directives are:

  • #define: creates a symbolic constant (although remember Effective C++, Item 2: Always prefer const to #define) or function macros.

  • Conditional compilation: allows or prevents certain lines of code to be compiled. Not-to-be-compiled lines are completely ignored by the compiler.

41

#define PI 3.14159

#define MIN(a,b) (((a)<(b)) ? a : b)

#ifdef DEBUG

// Something we only do in DEBUG mode

cerr <<"Variable x = " << x << endl;

#elifdef RELEASE

// Something we only do in RELEASE mode

cerr <<“Serious variable x = " << x << endl;

#endif

Search in Games

42 of 121

The Preprocessor Directives

  • Predefined macros:

  • #pragma once: prevents the compiler to read the file that includes this directive more than once.

42

#include <iostream>

int main () {

// Current line number of the program when it is being compiled

std::cout << "Value of __LINE__ : " << __LINE__ << std::endl;

// Current file name of the program when it is being compiled

std::cout << "Value of __FILE__ : " << __FILE__ << std::endl;

// String of the form month/day/year that is the date of the translation of the source file into object code.

std::cout << "Value of __DATE__ : " << __DATE__ << std::endl;

//String of the form hour:minute:second that is the time at which the program was compiled.

std::cout << "Value of __TIME__ : " << __TIME__ << std::endl;

return 0;

}

#pragma once

struct st {

int a;

}

#include “a.h”

a.h

b.h

#include “a.h”

#include “b.h”

c.h

Search in Games

43 of 121

Exceptions

Exception handling provides a mechanism to decouple handing errors from the typical control flow of the code. For instance, it’s useful to throw exceptions in a function when something unexpected happens that can’t be handled properly with a regular return value.

  • Example: a file that needs to be read with std::ifstream can’t be found in the filesystem.

The standard library provides utilities for throwing exceptions:

43

#include <exception>

int main () {

// ...

// Something wrong happens

if(veryBadError)

{

throw std::runtime_error(“Houston, we have a problem!”);

}

// This code will not be reached here if an exception is thrown

return 0;

}

Search in Games

44 of 121

Exceptions

Try-catch statements allow you to cleanly capture exceptions:

44

#include <exception>

double doubleThrower()

{

throw -10.0;

}

void exceptionThrower()

{

throw std::runtime_error("Houston, we have a problem!");

}

int main () {

try {

doubleThrower();

// This will not execute as previous line throws exception

exceptionThrower();

}

catch (double x)

{

std::cout << "I caught the exception: " << x << std::endl;

}

catch (std::runtime_error e)

{

std::cout << "I caught the exception " << e.what() << std::endl;

}

// This code will now be reached

return 0;

}

You can also throw a built-in type!

Exceptions thrown within the scope of the try {} will be captured in the respective catch {} blocks

This will capture exceptions like the one thrown by doubleThrower();

This will capture exceptions like the one thrown by exceptionThrower();

Search in Games

45 of 121

C++

Memory: Raw pointers

46 of 121

Memory: heap and stack

C++ allows you (requires you) to manage the memory the program uses.

Your C++ program has access to two types of (RAM) memory:

  • The stack, which has fast access but it’s regularly small (~MB).
    • This is private and given by the Operating System to the program on start.
    • Memory is deallocated when objects leave their scope.
    • Allocates by default local variables (not pointers), function calls.
    • Stack memory allocation sizes need to be computed at compilation time.
  • The heap, which has slower access but it’s generally larger (~GBs).
    • This is managed by the Operating System while the program runs.
    • Allocates variables with “new” and memory allocation which size is known only at runtime.
    • Contents access via a pointer.

C++ uses dynamic memory allocation for the heap: it requests memory from the OS when needed.

46

int year {2022}; //Variable allocated in the stack

int *year = new int(2022); //Variable (pointer) goes to the heap.

Search in Games

47 of 121

Memory Allocation

Types of memory allocation:

  • Dynamic memory allocation: when using new and delete.
  • Static memory allocation: for static and global variables and it’s deallocated only when the program finishes.
  • Automatic memory allocation: for function parameters and local variables, deallocated when out of scope.

In static and automatic allocation, the size of the variable or array must be known at compile time (it happens in the stack) and allocation/deallocation happens automatically when variables are instantiated/destroyed.

To allocate/deallocate a single variable dynamically, we use the new/delete operator:

When working with arrays of data, we use the array form of new and delete.

47

new int(2022); //Generates space for an int in the heap with a value

int *year = new int(2022); //Normally, we capture a pointer to that position in memory

delete year; //Deallocates the memory pointed by “year”

Important Note:

Wherever possible, rather than using new and delete, we’ll use Smart Pointers (we’ll see them later on). But new and delete are so widespread in C++ it’s very important to know they exist and the implications of using them (in)correctly.

int* array = new int[100]; //Pointer to an array with size 100

int* array2 {new int[3] {1,2,3}}; //Since C++11, you can declare and initialize arrays like this.

delete[] array; //Deallocates the memory pointed by the array pointer. Same for array2.

Search in Games

48 of 121

Memory Allocation

A pointer holds the address of an object of the appropriate type.

  • In a declaration, * means “pointer to”.
  • In an expression, * means “contents of” and the prefix & means “address of”.
  • Pointers to objects allow access to their member variables with the operand “🡪”

48

void func() {

}

Stack

Memory

(mem address)

0x0010

0x0011

0x0012

0x0013

Heap

Memory

(mem address)

0x0A12

0x0A13

0x0A14

0x0A15

Search in Games

49 of 121

Memory Allocation

49

void func() {

int n1 = 3; // a variable (stack allocated)

}

Stack

Memory

(mem address)

3

0x0010

0x0011

0x0012

0x0013

Heap

Memory

(mem address)

0x0A12

0x0A13

0x0A14

0x0A15

(n1)

A pointer holds the address of an object of the appropriate type.

  • In a declaration, * means “pointer to”.
  • In an expression, * means “contents of” and the prefix & means “address of”.
  • Pointers to objects allow access to their member variables with the operand “🡪”

Search in Games

50 of 121

Memory Allocation

50

void func() {

int n1 = 3; // a variable (stack allocated)

int* n2 = &n1 // n2 (pointer on the stack) points to n1 (still on stack).

}

Stack

Memory

(mem address)

3

0x0010

0x0010

0x0011

0x0012

0x0013

Heap

Memory

(mem address)

0x0A12

0x0A13

0x0A14

0x0A15

(n1)

(n2)

A pointer holds the address of an object of the appropriate type.

  • In a declaration, * means “pointer to”.
  • In an expression, * means “contents of” and the prefix & means “address of”.
  • Pointers to objects allow access to their member variables with the operand “🡪”

Search in Games

51 of 121

Memory Allocation

51

void func() {

int n1 = 3; // a variable (stack allocated)

int* n2 = &n1 // n2 (pointer on the stack) points to n1 (still on stack).

*n2 = 4; // assign a value of 4 to the memory pointed by n2.

}

Stack

Memory

(mem address)

4

0x0010

0x0010

0x0011

0x0012

0x0013

Heap

Memory

(mem address)

0x0A12

0x0A13

0x0A14

0x0A15

(n1)

(n2)

A pointer holds the address of an object of the appropriate type.

  • In a declaration, * means “pointer to”.
  • In an expression, * means “contents of” and the prefix & means “address of”.
  • Pointers to objects allow access to their member variables with the operand “🡪”

Search in Games

52 of 121

Memory Allocation

52

void func() {

int n1 = 3; // a variable (stack allocated)

int* n2 = &n1 // n2 (pointer on the stack) points to n1 (still on stack).

*n2 = 4; // assign a value of 4 to the memory pointed by n2.

int* n3 = new int(10); // Pointer is in the stack, object pointed to in the heap.

}

Stack

Memory

(mem address)

4

0x0010

0x0010

0x0011

0x0A12

0x0012

0x0013

Heap

Memory

(mem address)

10

0x0A12

0x0A13

0x0A14

0x0A15

(n1)

(n2)

(n3)

A pointer holds the address of an object of the appropriate type.

  • In a declaration, * means “pointer to”.
  • In an expression, * means “contents of” and the prefix & means “address of”.
  • Pointers to objects allow access to their member variables with the operand “🡪”

Search in Games

53 of 121

Memory Allocation

What happens when we finish this function?

Local variables run out of scope and are deallocated from the stack:

53

void func() {

int n1 = 3; // a variable (stack allocated)

int* n2 = &n1 // n2 (pointer on the stack) points to n1 (still on stack).

*n2 = 4; // assign a value of 4 to the memory pointed by n2.

int* n3 = new int(10); // Pointer is in the stack, object pointed to in the heap.

}

Stack

Memory

(mem address)

0x0010

0x0011

0x0012

0x0013

Heap

Memory

(mem address)

10

0x0A12

0x0A13

0x0A14

0x0A15

What happens with this??

  • Nobody is pointing at it.
  • It can’t be deallocated.

It’s a memory leak.

Search in Games

54 of 121

Memory Allocation

When we are done with dynamically allocated memory, we need to release (deallocate) it.

This is done via de delete operator.

  • It “tells” the Operating System that the memory the pointer is pointing at is now free.

Dangling pointers:

  • C++ doesn’t guarantee what happens with contents of deallocated memory.
  • Sometimes, more than one pointer can be pointing to the same memory address, and we delete one of them.
  • Then, the memory is deallocated but we still have a pointer pointing at it. That’s a dangling pointer.
  • Dangling pointers are bad. Undefined behaviour may happen if they are used – even deleting them!

54

void func() {

int n1 = 3;

int* n2 = &n1

*n2 = 4;

int* n3 = new int(10);

//...

delete n3;

}

Do not delete n2. This is not dynamically allocated,

bad things can happen!

This returns the memory pointed by n3 to the operating system (funnily enough, it actually doesn’t delete anything).

Search in Games

55 of 121

Null Pointers

All pointers have to point to an object so that dereferencing it is valid.

If we have no objects to point to, we give the pointer the value nullptr (null pointer). A null pointer basically says “no memory has been allocated to this pointer”.

nullptr pointers don’t need to be deleted.

55

double* pd = nullptr; // pointer to a double with value nullptr

vector<Record>* lst = nullptr; // pointer to a vector of Record objects with value nullptr

int x = nullptr; // error: nullptr is a pointer, not an integer.

if(pd == nullptr) {...} // we can check if a pointer has the nullptr value.

0 and NULL can be interpreted as integers, which may create inconsistencies.

nullptr is always a pointer.

Effective Modern C++, Item 8: Prefer nullptr to 0 or NULL

Search in Games

56 of 121

C++

Memory: References

57 of 121

References

By default, C++ passes objects to and from function by value.

  • Unless you specify otherwise, function parameters are initialized with copies of the actual arguments, produced by the object’s copy constructor.
    • EXCEPTION: arrays are not passed by value in function arguments!
  • This can make passing by value an expensive operation

A reference is similar to a pointer, but we don’t use * to access the value referred to by the reference.

References are particularly useful when specifying function arguments or return values:

  • By passing (or returning) a reference, no copies are being made, so modifications of the corresponding variables will take effect on the original ones.

  • If we want to avoid arguments / return values to be modified, we can use a const reference.

57

void sort(Object& v); // the object v is not copied when passed to the function.

Object& getContent(); // this function returns a reference (not a copy) of a variable

void sort(const Object & v); // the object v is a const reference that can’t be modified inside the function.

const Object& getContent(); // this function returns a const reference that can’t be modified by the caller.

Search in Games

58 of 121

References

Slicing problem: if a derived class object is passed by value as a base class object, the copy constructor on the base class is the one that is called. The derived class object is “sliced” off.

58

It’s more efficient and avoids the slicing problem.

This does not apply to built-in types, STL iterators and STL function objects (they don’t have this problem).

Effective C++, Item 20: Prefer pass-by-reference-to-const to pass-by-value

Window

ScrollableWindow

// Pass by value. (Copy) constructor of Window

// is called to generate ‘w’

void doSomethingWithWindow(Window w)

{

//Some code

}

// When this function is called, we lose the

// definitions of the derived class;

ScrollableWindow sw;

doSomethingWithWindow(sw);

// Pass by reference to const

// No copy is being made, nothing is lost.

void doSomethingWithWindow(const Window& w)

{

//Some code

}

Search in Games

59 of 121

References

You can also avoid calls to the constructor of an object returning a reference from a function:

However, you should not return a reference of or a pointer to an object created inside the function.

59

Object getContent(); // Returns an object making use of Object’s copy constructor. Returns a copy.

Object& getContent(); // Returns a reference that can be modified by the caller.

const Object& getContent(); // Returns a const reference that can’t be modified by the caller.

Object* getContent(); // Returns a pointer to the object.

Local variables have a limited scope, reduced to the function in which they are created.

A pointer or a reference to a local variable returned from a function will be pointing at a destroyed object.

This, in turn, will cause undefined behaviours.

Effective C++, Item 21: Don’t return a local variable with a reference or a pointer

Search in Games

60 of 121

References and pointers

An assignment of a built-in type is a simple machine copy operation:

In this example, x and y are still independent. They can be made dependent with pointers:

60

int x {2};

int y {3};

x = y; // the value of y is copied into x

x:

2

y:

3

x:

3

y:

3

x=y;

int x {2};

int y {3};

int* p = &x; // p contains the address of x

int* q = &y; // q contains the address of y

p = q; // p becomes &y.

p = q;

x:

2

y:

3

p:

0x88

q:

0x91

x:

2

y:

3

p:

0x91

q:

0x91

p==q and *p == q

Search in Games

61 of 121

References and pointers

Both a reference and a pointer refer/point to an object and both are represented in memory as an address:

Assignment to a reference does not change what the reference refers to but assigns to the referenced object.

61

int x {2};

int y {3};

int& r = x; // r refers to x

int& r2 = y; // r2 refers to y

r = r2; // read through r2, write through r. x becomes y.

x:

2

y:

3

r = r2;

r:

0x88

r2:

0x91

x:

3

y:

3

p:

0x88

q:

0x91

Search in Games

62 of 121

auto, const, and references

Type deduction drops const qualifiers. Thus, const must be provided when using auto if new variable should be const:

Type deduction also drops references. Thus, & must be provided when using auto if new variable should be a reference:

62

int main()

{

const int x {5}; // ‘x’ is a const int variable

auto y = x; // ‘y’ is an int variable (non-const!)

const auto z = x; // ‘z’ is type const int

}

int main()

{

int x {5}; // ‘x’ is an int variable

int& y = x; // ‘y’ is a reference to an int variable

auto z = y; // ‘z’ is an int variable, not an int&

auto& w = x; // ‘w’ is type int&

}

Search in Games

63 of 121

C++

Memory: Smart Pointers

64 of 121

Memory Allocation: Smart Pointers

Traditional (raw) pointers have many drawbacks:

  • It’s declaration doesn’t indicate whether it points to a single object or an array.

  • It’s not clear if a pointer is the only one pointing to that address of memory by looking at its declaration (remember dangling pointers).
  • How and when do you delete a pointer? Should you use delete or delete[]?
  • You need to deallocate a pointer just once: never deallocating creates memory leak, multiple deallocations cause undefined behaviour.
  • There is no way to know if a pointer is dangling or if it’s still valid.

While raw pointers are powerful, they’re the cause of many errors in C++.

Smart pointers are wrappers around raw pointers that are useful to avoid the problems above.

64

int* n3; //array or single object?

Problems with raw pointers and history on smart pointers.

Effective Modern C++, Chapter 4 (Introduction)

Search in Games

65 of 121

Memory Allocation: Smart Pointers

Smart pointers are composition classes that are designed to manage dynamically allocated memory, making sure that memory gets deleted when necessary (we don’t need to explicitly delete them).

The most common types of smart pointers are:

  • std::unique_ptr: represents unique ownership of memory.
    • It handles an individual object (or array).
    • By default, they have the same size as a raw pointer.
    • They use move semantics. Moving is always faster than copying.
  • std::shared_ptr: represents a shared ownership of memory.
    • Contains a counter of objects owning this pointer. This counter:
      • Is increased when a new object points at the resource.
      • Is decremented when the object that owns it falls out of scope.
    • Object pointed at is destroyed when nobody points at it (i.e. when the counter is 0).
    • They are double the size of a raw pointer, as it stores a counter of objects using this pointer.
    • They are copied rather than moved.

65

Search in Games

66 of 121

Memory Allocation: Smart Pointers

Example:

How to access the managed resource from a smart pointer?

  • Both the -> operator and the function get() return a raw pointer to the managed resource.
  • The * operator returns a reference to the managed resource.

66

#include <memory>

std::unique_ptr<int> intPtr(new int (5)); //Unique pointer

std::cout << "Val: " << *intPtr << std::endl;

std::shared_ptr<Vector2d> v1 (new Vector2d(0.1f, 0.5f) ); //Shared pointer

std::cout << "Vec x: " << v1->toString() << std::endl;

Vector2d* vPtr = v1.get(); //Obtain a pointer to the managed resource

std::string strViaPtr = v1->toString(); //Access resource via a pointer (-> operator)

std::string strViaRef = (*v1).toString(); //Access resource via a reference (* operator)

Search in Games

67 of 121

Memory Allocation: Smart Pointers

Common pitfalls in std::unique_ptr and std::shared_ptr:

  • Do not let multiple pointers manage the same resource:

  • The same applies to std::shared_ptr!

67

Vector2d* vect { new Vector2d() };

std::unique_ptr<Vector2d> v1 {vect};

std::unique_ptr<Vector2d> v2 {vect};

Syntactically, this is correct, but both pointers will try to delete vect, leading to undefined behaviour

  • Do not manually delete the resource:

Vector2d* vect { new Vector2d() };

std::unique_ptr<Vector2d> v1 {vect};

delete vect;

Again, syntactically correct, but v1 will try to delete vect, leading to undefined behaviour

v1 and v2 are unaware of each other. When v1’s counter gets to 0, it’ll delete the resource leading to undefined behaviour

Vector2d* vect { new Vector2d() };

std::shared_ptr<Vector2d> v1 {vect};

std::shared_ptr<Vector2d> v2 {vect};

Vector2d* vect { new Vector2d() };

std::shared_ptr<Vector2d> v1 {vect};

delete vect;

v1 will try to delete vect when its counter of managers get to 0, leading to undefined behaviour

Search in Games

68 of 121

Memory Allocation: Smart Pointers

Creating an object and passing its pointer to a smart pointer is verbose and error prone. To avoid this, the standard <memory> library provides functions for constructing these pointers: std::make_unique and std::make_shared.

68

#include <memory>

auto vec1 = std::make_unique<Vector2d>();

std::cout << vec1->toString() << std:endl;

auto vec2 = std::make_shared<Vector2d>(2.1f, 4.0f);

std::cout << vec2->toString() << std:endl;

The make_x functions eliminate source duplication, improve exception safety and it’s generally faster.

However, make_x functions may be inappropriate when implementing custom deleters and custom memory C++ management modules.

If make_x functions can’t be used, build standalone statements for storing “new” objects into smart pointers (see Effective C++, item 17).

Effective Modern C++, Item 21: Prefer make_unique and make_shared to direct use of new

Search in Games

69 of 121

C++

Object-Oriented Principles:

User defined types, constructors and destructors

70 of 121

User-defined Types: enum classes

The simplest user-defined data type in C++ is the enumerated (or enumeration, or enum) type. In an enum, every possible value of the type is defined as a symbolic constant.

70

enum class Color // An enum class

{

red, //Different values for this type

yellow,

green,

blue

};

// Types defined with the enum class name prefix

Color c{ Color::red };

// Values can be compared.

bool isRed = (c == Color::red);

// Doing a static cast of the variable to int retrieves its index in the enum

int ordinal = static_cast<int>(c); //value of ’ordinal’ will be 0

Unscoped enums are those with no “class” keyword in their definition

They are unscoped, what means that their symbolic constant names can’t be used anywhere else.

Effective Modern C++, Item 10: Prefer scoped enums to unscoped enums

Search in Games

71 of 121

User-defined Types: unions

A union is a special class type that can hold only one of its non-static data members at a time.

The declaration is very similar to the one found in enums:

The union is as big as necessary to hold its largest data member. In memory, the other data members are allocated in the same space as this largest data member: all non-static data members have the same memory address.

Out of all the members of a union, there is only one that can be active. Generally speaking, a member is made active when a value is assigned to it:

71

union CustomDatatype // A union

{

std::int32_t integer; // This occupies 4 bytes

std::uint16_t uiarr[2]; // This occupies 4 bytes

std::uint8_t shortint; // This occupies 2 bytes.

}; // The whole union occupies 4 bytes.

CustomDatatype d;

d.integer = 10; // “integer” is the active data member.

std::cout << “Value: ” << d.integer << std::cout; // Prints “10”.

Search in Games

72 of 121

User-defined Types: Struct

A struct is a data structure that allows you to combine (potentially different) data items into a type.

Structs also allow the declaration of functions:

72

struct Date

{

int y; //year

int m; //month in year

int d; //day of month

}

Date today; //a Date variable (named object)

The members of a struct (in the example, three integers) can be read and written:

and their functions called:

today.y = 2021;

today.m = 9;

today.d = 17;

struct Date

{

int y; //year

int m; //month in year

int d; //day of month

int month() {return m;}

}

std::cout << today.month() << std:: endl;

std::cout << today->month() << std:: endl;

Use . (dot) to access struct members through a name or reference.

Use -> to access struct members through a pointer.

Search in Games

73 of 121

User-defined Types: Classes

While structs are closely related to data, classes are useful when operations with this data and their representation are tightly related.

Normally, classes provide access to data to those who want to use it while keeping its representation inaccessible. This differentiates between the interface (how to use and access a class) and the implementation (how it operates internally).

  • The interface is defined by the public members of the class.
  • The private members of the class are only accessible via this interface.

Example:

73

class Vector {

public:

Vector(int s) : elem{new double[s]}, sz{s} {} //This is the Vector class constructor

double& operator[] (int i) {return elem[i];} //Element access (via operator overlading)

int size() {return sz;} //Number of elements of the vector

private:

int sz; //Private variable to keep number of elements

double* elem; //Pointer to all elements of the vector

};

Search in Games

74 of 121

Constructors and Destructors

A constructor is a special type of class member function that is automatically called when an object is created. Similarly, a destructor is executed when an object of the class it belongs to is destroyed.

  • Constructors should initialize objects so they are well-defined and ready to be used.
  • Destructors must clean up and free resources used by the object.

Both functions must be named the same as the class, with destructors preceded by a tilde (~). None of them has return types and only the constructor may receive arguments (i.e. there can be only one destructor).

74

class Vector {

public:

Vector(int s) : elem{new double[s]}, sz{s} {} //This is a Vector class constructor

Vector() : elem{new double[0]}, sz{0} {} //This is the default class constructor (no parameters)

~Vector() {} //This is the class destructor

// ...

private:

int sz; //Private variable to keep number of elements

double* elem; //Pointer to all elements of the vector

};

Search in Games

75 of 121

Constructors and Destructors

A constructor that takes no parameters is called a default constructor.

75

class Vector {

public:

Vector() : elem(nullptr), sz(0) {} //This is a Vector class default constructor.

Vector(int s) : elem(new double[s]), sz(s) {} //This is a Vector class constructor with arguments.

private:

int sz; //Private variable to keep number of elements

double* elem; //Pointer to all elements of the vector

};

//Copy constructor.

Vector(const Vector& otherV) {}

//Copy assignment operator

Vector& operator=(const Vector& rhs) {}

Furthermore:

  • A copy constructor is a member function that initializes an object by making a copy of another object of the same class.
  • A copy assignment operator is a member function that overrides the operator “=”, copying one class object to another existing class object.

Search in Games

76 of 121

Constructors and Destructors

A constructor that takes no parameters is called a default constructor.

Constructors may incorporate initializer lists to set values to members of the class.

76

class Vector {

public:

Vector(int s) : elem(new double[s]), sz(s) // This is a Vector class constructor with an initializers list.

{} // -> Note the constructor’s body is empty.

Vector(int s) { // This is a Vector class constructor without an initializers list

sz = s; // -> these are all assignments, not initializations

elem = new double[s];

}

private:

int sz; //Private variable to keep number of elements

double* elem; //Pointer to all elements of the vector

};

Initializer lists are more efficient: a constructor without an initializers list first calls default constructors to initialize the members of the class, then it makes the assignments (wasting the initial computation).

The initializer lists avoids this: the arguments in the initialization list are used as constructor arguments for the different data members.

Effective C++, Item 4. Prefer initializer lists in constructors to assignments

Search in Games

77 of 121

Constructors and Destructors

These functions are called automatically when objects of the class are created, copied or destroyed:

  • If these functions are not declared, the compiler will declare default versions of a copy constructor, a copy assignment operator and a destructor. If no constructors are declared at all, the compiler will also create a default constructor for the class.
  • When dealing with dynamically allocated memory, it is recommended that these functions are provided explicitly. Especially in the case of the copy constructor and operator, where the compiler-created functions do shallow copies.

77

These functions are generated only if needed, but they are easily needed!

The automatically generated constructor and destructor will contain the respective invocations to base classes and non-static data members. The generated copy assignment and constructor will copy each non-static data member of the source object over the target one.

Effective C++, Item 5: C++ silently writes and calls some of these functions

Vector v1; //Calls default constructor (and eventually, default destructor when object falls out of scope).

Vector v2(v1); //Calls copy constructor

v2 = v1; //Calls copy assignment operator.

Search in Games

78 of 121

C++

Object-Oriented Principles:

Inheritance, visibility and polymorphism

79 of 121

Object-Oriented Principles: inheritance

Inheritance establishes an “is-a” relationship between two classes

79

Character

Orc

Guard

Parent class, Base class, Superclass

Child class, Derived class, Subclass

The child inherits behaviours and properties from the parent class. These variables and functions become members of the derived class.

class Character{

public:

std::string name;

int strength;

Character(const std::string& n = "", int str = 0)

: name(n), strength(str) {}

const std::string& getName() const { return name; }

int getStrength() const { return strength; }

};

Base class Enemy

class Orc : public Character

{

public:

std::string orcClan;

Orc(const std::string& n = "", const std::string& oC = "", int str = 0)

: Character(n, str), orcClan(oC) {}

const std::string& getClan() const { return orcClan; }

void sayHi(){

std::cout << "I'm " << name << " from " << orcClan << std::endl;

}

};

Derived class Orc

Search in Games

80 of 121

Object-Oriented Principles: construction

In this example, we can call the derived class function to see existing access to the base class properties:

How are constructors called in a hierarchy of objects?

80

Orc c("Peter", "Highlands", 40);

c.sayHi();

Outputs

I'm Peter from Highlands

“Peter” is the value of property name in class Character

“Highlands” is the value of property orcClan from Orc

class Character{

public:

Character() { std::cout << "Character default constructor\n"; }

Character(const std::string& n = "", int str = 0)

: name(n), strength(str) {

std::cout << "Character parameterized constructor\n";

}

};

class Orc : public Character{

public:

Orc() { std::cout << "Orc default constructor\n"; }

Orc(const std::string& n, const std::string& oC, int str)

: Character(n,str), orcClan(oC) {

std::cout << "Orc parameterized constructor\n";

}

};

Orc c("Peter", "Highlands", 40);

Character parameterized constructor

Orc parameterized constructor

(1)

(1)

(2)

(2)

Orc c;

Character default constructor

Orc default constructor

(3)

(3)

(4)

(4)

First, the Base portion of the object is constructed (Base class, here: Character)

Then, the Derived potion of the object is constructed (Derived class, here: Orc)

(output)

Search in Games

81 of 121

Object-Oriented Principles: construction

When a derived class is instantiated, the following happens in order:

  1. Memory for the object Derived is reserved (enough size for both Base and Derived object sizes).
  2. The appropriate Derived constructor is called.
  3. The Base object is constructed using the appropriate Base constructor (including default constructor).
  4. The member initializer list initializes variables in Derived.
  5. The body of the Derived constructor executes.
  6. Control is returned to the caller.

Note that the derived class initializes properties in the base via a constructor:

81

Orc(const std::string& n, const std::string& oC, int str)

: Character(n,str), orcClan(oC) {

// Identifies constructor in base class to call

// This is the preferred way to do this.

}

, because:

Orc(const std::string& n, const std::string& oC, int str)

: name(n), strength(str), orcClan(oC) {

// Compilation error: we can’t do this! (can violate const restrictions)

}

Orc(const std::string& n, const std::string& oC, int str) : orcClan(oC) {

name = n;

strength = str;

// This is possible, but may not work if properties are const

}

Search in Games

82 of 121

Object-Oriented Principles: visibility

Classes can control the access they provide to their subclasses (and all other objects):

82

class Character{

public:

std::string name;

protected:

int strength;

private:

int age;

Character(const std::string& n = "", int str = 0)

: name(n), strength(str) {}

public:

const std::string& getName() const { return name; }

int getStrength() const { return strength; }

friend void operate();

};

void operate() { /* Does something */ }

Can be accessed by Base members, friends and derived classes.

Can be accessed by Base members and friends.

Access can be controlled for properties and functions.

Names in derived classes hide names in base classes. With public inheritance, this is never desirable.

To make hidden names visible again, employ using declarations or forwarding instructions.

Effective C++, Item 33: Avoid hiding inherited names

Can be accessed by any other object

A friend function has access to private members of the class.

  • This friend function is not a member of the class.

Search in Games

83 of 121

Object-Oriented Principles: overriding

By default, derived classes inherit behaviours from their base classes. When a member function is called, the compiler first checks the functions in the derived classes and, if no match is found, it moves up the hierarchy towards base classes.

83

class Character

{

// ...

public:

void sayHi() const {

std::cout << "I'm "<< name << ", a Character.\n";

}

};

class Orc: public Character

{

// ...

public:

void sayHello() const {

std::cout << "I'm " << name << ", an Orc.\n";

}

};

Character c;

c.sayHi();

// ...

Orc o;

o.sayHello();

We can also redefine behaviours by overriding functions in the derived class from the base class:

class Character

{

// ...

public:

void sayHi() const {

std::cout << "I'm "<< name << ", a Character.\n"; }

};

class Orc : public Character

{

// ...

public:

void sayHi() const {

std::cout << "I'm " << name << ", an Orc.\n";

// We can call the base class method:

// Character::sayHi();

}

};

Character c;

c.sayHi();

// ...

Orc o;

o.sayHi();

Search in Games

84 of 121

Object-Oriented Principles: overriding

The object of a given type (class) have access to the properties and function of that class:

c1 and c2 are of class Character. They don’t have access to the properties of Orc.

This is not very convenient, because we couldn’t do things like this:

84

Orc o1("Peter", "Highlands", 40);

o1.sayHi();

Orc& o2{ o1 };

o2.sayHi();

Orc* o3{ &o2 };

o3->sayHi();

Outputs

I'm Peter, an Orc

I'm Peter, an Orc

I'm Peter, an Orc

Orc o1("Peter", "Highlands", 40);

o1.sayHi();

Character& c1{ o1 };

c1.sayHi();

Character* c2{ &o1 };

c2->sayHi();

I'm Peter, an Orc

I'm Peter, a Character

I'm Peter, a Character

Outputs

std::vector<Character*> team;

team.push_back(new Orc("Peter", "Highlands", 40));

team.push_back(new Orc("Mark", "Lowlands", 20));

team.push_back(new Guard("Elena", 30));

for (auto c : team)

c->sayHi();

I'm Peter, a Character

I'm Mark, a Character

I'm Elena, a Character

Outputs

Search in Games

85 of 121

Object-Oriented Principles: polymorphism

Polymorphism: A virtual function is a type of function that resolves to the most-derived function in a hierarchy.

  • A virtual function is a match to a parent class function if it has the same signature (parameters and return types).
  • In that case, the virtual function overrides the function in the parent class.

Our character example with virtual functions would look like this:

85

class Character{

public:

std::string name;

int strength;

Character() {

std::cout << "Character default constructor\n";

}

virtual void sayHi() const {

std::cout << "I'm " << name << ", a Guard\n";

}

final void sayHello(){

std::cout << “Only in Character!\n";

}

};

class Orc : public Character

{

// ...

public:

virtual void sayHi() const override {

std::cout << "I'm " << name << ", an Orc.\n";

}

};

class Guard : public Character

{

// ...

public:

virtual void sayHi() const override {

std::cout << "I'm " << name << ", a Guard.\n";

}

};

std::vector<Character*> team;

team.push_back(new Orc("Peter", "Highlands", 40));

team.push_back(new Orc("Mark", "Lowlands", 20));

team.push_back(new Guard("Elena", 30));

for (auto c : team)

c->sayHi();

I'm Peter, an Orc

I'm Mark, an Orc

I'm Elena, a Guard

Outputs

The override keyword is not required, but if forces the compiler to verify that this function does indeed override the base class function.

The compiler will give an error if the signature is not compatible with a function in the base class.

The final keyword prevents this function to be overridden in any sub-class.

Search in Games

86 of 121

Object-Oriented Principles: virtual functions

Notes on virtual functions:

  • Resolving a call to virtual functions takes longer than non-virtual functions. Use them sparingly!
  • Do not call virtual functions from constructors or destructors!

  • But destructors in derived classes must be virtual. Otherwise, they won’t be called when deleting derived class objects.

86

Constructors of a base class are called before constructors of the derived class.

  • The derived class is not initialized yet, we can’t call its functions! The object is of the base class type yet.

Destructors of a derived class are called before destructors of a base class (opposite order to constructors).

  • The derived object’s members are deleted once its destructor is called.

In both cases, virtual functions are resolved to the Base class, not the derived one(s).

Effective C++, Item 9: Never call virtual functions during construction or destruction

Search in Games

87 of 121

Object-Oriented Principles: pure virtual

A pure virtual function (or an abstract function) is a function with no body - it’s only declared.

If a class has at least one pure virtual function, that class is said to be an abstract class.

Abstract classes can’t be instantiated.

An interface is a special class that has no member variables, and all its functions are pure virtual.

87

class Character

{

// Normal, non-virtual function

void sayHi () const { std::cout << “Hi\n”; }

// Normal virtual function.

virtual const std::string getName() const {return name;}

// A pure virtual function

virtual int getValue() const = 0;

// Compilation error: can’t set a non-virtual function to 0

int doSomething() = 0;

};

Search in Games

88 of 121

C++

Object-Oriented Principles:

Forward declarations and function pointers

89 of 121

Function Pointers

A function pointer is a variable that stores the address of a function that can later be called through that pointer. A classical use of function pointers (i.e. in games) is to use them as callbacks when some event happens.

The classical function pointer syntax takes this form:

For example, the following code shows a function pointer declaration and usage:

89

return_type (*function_name) (params);

// Function pointer via typedef to make for easier reading

typedef bool (*FuncPtrBoolInt)(int);

// A compatible function with this function pointer

bool updateProgress(int pct) {

std::cout << pct << "% complete...\n";

return true;

}

// A function that receives and calls a function pointer by parameter

void operate(FuncPtrBoolInt func) {

for (long l = 0; l < 100000000; l++)

if (l % 10000000 == 0)

func(l / 1000000);

}

int main(){

operate(updateProgress);

}

From https://www.oreilly.com/library/view/c-cookbook/0596007612/ch15s02.html

Search in Games

90 of 121

Function Pointers

An issue with the function pointers as defined above is that they are very rigid with respect to the function, return and parameter types. In the example from the previous slide:

  • We must use a function (not, for instance, a member function).
  • We must pass an int parameter (not any other type, even if convertible to an int).

The standard library provides a more generic function pointer (std::function) that uses templates to address this limitation.

The syntax of std: function looks like this:

90

std::function<return_type(params)> function_name;

Search in Games

91 of 121

Function Pointers

The previous example with std::function could look like this:

91

#include <functional>

// Function pointer via typedef to make for easier reading

typedef std::function<bool(int)>Update;

// A compatible function with this function pointer

bool updateProgress(int pct) { /* Same as before */ }

// A function that receives and calls a function pointer by parameter

void operate(Update func) {

for (long l = 0; l < 100000000; l++)

if (l % 10000000 == 0)

func(l / 1000000);

}

int main(){

operate(updateProgress);

}

There are several alternatives to virtual functions that are more flexible.

In particular, std::function can replace virtual functions allowing use of any callable entity with a signature compatible with what’s needed (this is one form of the Strategy pattern).

Effective C++, Item 35: Consider alternatives to virtual functions

Search in Games

92 of 121

Function Pointers

Using function pointers for member functions requires binding them to the instances of the objects that have those functions. For this, we use std::bind:

For example, to bind a function pointer of the type std::function<void(void)>, we would do something like the following from the object itself:

Here is another example for a function with several parameters, where we would do:

  • std::placeholders::_1 and std::placeholders::_2 indicate std::bind that the function pointer has two parameters.

92

std::bind(function, instance, argumentlist ...);

std::function<void(void)> f = std::bind(&Class::Function, this);

// For std::function<void(int, float)>

std::function<void(int, float)> f = std::bind(&Class::Function, this, std::placeholders::_1 , std::placeholders::_2);

Search in Games

93 of 121

Forward Declarations

In some cases, functions (or classes) need to be forward declared so the compiler can validate the code in a single file.

  • This is particularly useful with two classes need to reference each other, avoiding an infinite loop of includes.
  • The compiler will need to find the definition of the class/function in another file.

  • It can also be used for functions:

93

#include “Sword.h”

class Character

{

public:

// Character stuff

private:� Sword* skullOpener;

};

Character.h

#include “Character.h”

class Sword

{

public:

// Sword stuff

private:� Character* owner;

};

Sword.h

Trouble

  • Instead:

//Forward declaration

class Sword;

class Character

{

public:

// Character stuff

private:� Sword* skullOpener;

};

Character.h

#include “Character.h”

class Sword

{

public:

// Sword stuff

private:� Character* owner;

};

(Unchanged) Sword.h

void myForwardDeclaredFunction(bool);

int main() {

myForwardDeclaredFunction(true);

}

Search in Games

94 of 121

C++

The Standard (Template) Library

95 of 121

The Standard (Template) Library

The Standard library contains a collection of classes that provide templated containers, algorithms and iterators. The most commonly used functionality of the STL library are the STL Container classes.

Sequence containers: they hold a set of elements in the container in a specific order:

  • std::vector<T>: dynamic array of variable size. Has a [] operator for access and insertion/removing functions.
  • std::deque<T>: double-ended queue class of variable size that can grow at both ends. Insertion/deletion via functions and access [] operator.
  • std::list<T>: a double linked list, no access [] operator defined.
  • std::string: a vector with data elements of type char.

Associative containers: elements are automatically sorted when inserted in the structure:

  • std::set<T>: stores unique elements, not allowing duplicates, sorted according to their values.
  • std::multiset<T>: stores elements sorted according to their values, allowing duplicates.
  • std::map<K,T>: stores pairs of elements, which are key/pair tuples. Key must be unique and Value is the data.
  • std::multimap<K,T>: like an std::map<K,T>, but keys may be repeated.

95

Search in Games

96 of 121

The Standard (Template) Library

Container Adapters: containers adapted for specific uses.

  • std::stack<T>: container where elements operate in a LIFO manner.
  • std::queue<T>: container where elements operate in a FIFO manner.
  • std::priority_queue<T>: a queue where elements are sorted and removing the front of the queue returns the top priority one.

Also in the standard library:

  • Smart pointers (std::unique_ptr, std::shared_ptr)
  • Standard IO (std::cout)
  • Function pointers (std::function, std::bind)
  • Date and time utilities (std::chrono)
  • Hashing (std::hash)
  • Pairs, tuples (std::pair, std::tuple)
  • Iterators (std::iterator)
  • Random number generators (std::rand)

96

Search in Games

97 of 121

Iterators

An iterator is an object that can traverse a container class without the user needing to know how is the container internally implemented. An iterator can be understood as a pointer to one element in the container, which we can move, advance, and use to retrieve the element in the container.

Iterators provide a set of overloaded operators:

  • Operator*: Dereferences the element the iterator is pointing at.
  • Operator++ / Operator--: Advances the iterator to the next / previous element in the container.
  • Operator== / Operator!=: Determines if two iterator point at the same elements.
    • Note that this does not compare the elements, but the pointers! To compare the elements, first use “*”
  • Operator=: Assignment operator to set the position of the iterator to a new position.

97

#include <vector>

// ...

std::vector<int>::iterator vectorIterator;

Search in Games

98 of 121

Operator Overloading

In C++, operators (+, -, ==, etc.) are implemented as functions. The same as with functions, we can overload them:

98

class Vector2D {

private:

float _x, _y;

public:

Vector2D() :_x(0.f), _y(0.f) {}

Vector2D(float x, float y) :_x(x), _y(y) {}

float x() const {return _x;}

float y() const {return _y;}

Vector2D operator+(const Vector2D& v1, const Vector2D& v2){

Vector2D sum(v1.x() + v2.x(), v1.y() + v2.y());

return sum;

}

bool operator== (const Vector2D& v1, const Vector2D& v2){

return (v1.x() == v2.x()) && (v1.y() == v2.y());

}

};

Vector2D one(10.f, 2.f);

Vector2D two(20.f, 5.f);

Vector2D sum = one + two;

bool eq = (one == two);

https://www.learncpp.com/cpp-tutorial/introduction-to-operator-overloading/

#include <vector>

std::vector<int> myNumbers;

myNumbers.push_back(10);

myNumbers.push_back(-5);

//initialization

std::vector<int>::iterator vectorIterator = vector.begin();

// Have we reached the end?

bool reachedEnd = (vectorIterator == vector.end(););

// Advance:

vectorIterator++;

// Access the element

int value = (*vectorIterator);

Search in Games

99 of 121

Iterators

Iterators also provide a set of basic functions:

  • begin(): returns an iterator that points to the first element of the container
  • end(): returns an iterator that points just after the last element of the container
  • cbegin(): returns a const iterator that points points to the first element of the container.
  • cend(): returns a const iterator that points points just after the last element of the container.

Therefore, iterators can be of two types:

  • container::iterator: a normal iterator that provides read and write access.
  • container::const_iterator: an iterator that provides only read access.

99

In the general case, if you do not need to modify what the iterator points to, always use const_iterator (for the same reasons you should use const when you don’t need to modify the corresponding variable).

Effective Modern C++, Item 13: Prefer const_iterator to iterator

Search in Games

100 of 121

Iterators

Example of using an iterator over a std::vector

100

#include <vector>

#include <iostream>

std::vector<int> vect;

for(int c=0; c < 6; ++c) vect.push_back(c); // Declare a vector with some values in it

std::vector<int>::const_iterator it; // Access a const iterator from the vector. We won’t be changing anything.

it = vect.cbegin(); // Place the iterator at the first element of the vector

while(it != vect.cend()) { // Check that we haven’t passed the last element of the vector

std::cout << *it << “ “; // Access the element pointed by the iterator

++it; // Advance the iterator to the next element

}

std::cout << std::endl;

Search in Games

101 of 121

Iterators

Example of using an iterator over a std::map:

101

#include <map>

#include <string>

#include <iostream>

std::map<int, std::string> themap; // Declare a map with some key-value pairs

themap.insert(std::make_pair(1, “health”));

themap.insert(std::make_pair(3, “strength”));

themap.insert(std::make_pair(5, “stamina”));

auto it = themap.cbegin(); // Place the iterator at the first element of the vector

// Note the use of auto to avoid typing std::map<int, std::string>::const_iterator;

while(it != themap.cend()) { // Check that we haven’t passed the last element of the vector

std::cout << it->first << “ is “ << it->second << “ ”; // The “->” operator access the key (through ‘first’) and the value (‘second’).

++it; // Advance the iterator to the next element in the map.

}

std::cout << std::endl;

Search in Games

102 of 121

Template Functions

Function templates are those that allow you to simplify the code to avoid creating multiple functions where the only difference is the type of the parameters received.

  • A template describes how a function (or a class, as we’ll see next) looks like, but using placeholders as types.
  • The compiler uses a template to generate a family of related functions or classes, each using different types.

This is an example of a function template:

And here’s an example of how is it used:

Rather than defining a function for each type, we implement a single function for all types.

102

template <typename T> // this is the template parameter declaration: this tells the compiler that this is a template with type T

T max(T x, T y){ // this is the function template definition for max<T>

return (x > y) ? x : y;

}

int maxValInt = max<int>(1,2); // When the compiler sees this, it creates a function int max(int, int) from the template.

float maxValFloat = max<float>(1.0f, 2.0f); // Analogously, the compiler creates a function float max(float, float) when it finds this.

Search in Games

103 of 121

Template Functions

An example from our lab code:

103

template <typename T>

std::shared_ptr<T> buildEntityAt(const std::string& filename, int col, int row);

Game.h

template <typename T>

std::shared_ptr<T> Game::buildEntityAt(const std::string& filename, int col, int row)

{

auto ent = std::make_shared<T>();

float x = col * spriteWH * tileScale;

float y = row * spriteWH * tileScale;

float cntrFactor = (tileScale - itemScale) * spriteWH * 0.5f;

auto positionComponent = dynamic_cast<PositionComponent*>(ent->getComponent(ComponentID::POSITION));

positionComponent->setPosition(x + cntrFactor, y + cntrFactor);

ent->init(filename, std::make_shared<SpriteGraphicsComponent>(itemScale));

return ent;

}

Game.cpp

Search in Games

104 of 121

Template Classes

We can also have template classes.

104

template <typename T>

class Array

{

private:

int length{};

T* data{};

public:

Array(int l){

length = l;

data = new T[length]{};

}

Array(const Array&) = delete;

Array& operator=(const Array&) = delete;

~Array() { delete[] data; }

void erase() {

delete[] data;

data = nullptr;

length = 0;

}

T& operator[](int index){

return data[index];

}

int getLength() const;

};

Template for this class

Type for the data of this class

The standard (template) library is full of template classes:

  • std::vector<T>
  • std::map<K,T>
  • std::unique_ptr<T>

Template classes should have all their content in the headers, without using .cpp files (linker error).

Array.h

Array<int> a{ 5 };

a[0] = 7;

std::cout << a[0] << std::endl;

Array<float> b{ 5 };

b[0] = 1.35f;

std::cout << b[0] << std::endl;

Search in Games

105 of 121

Template metaprogramming

How do templates really work?

  • The compiler transforms the templates that are used into assembly code. These templates themselves do not exist as generic code after compilation.
  • If our function:

  • is used in our code three times:

  • the compiler will create (in assembly) three versions of the buildEntityAt, one per type T used:

105

template <typename T>

std::shared_ptr<T> buildEntityAt(const std::string& filename, int col, int row);

std::shared_ptr<Log> log = buildEntityAt(/* some params */);

std::shared_ptr<Potion> log = buildEntityAt(/* some params */);

std::shared_ptr<Fire> log = buildEntityAt(/* some params */);

std::shared_ptr<Log> buildEntityAt(const std::string& filename, int col, int row);

std::shared_ptr<Potion> buildEntityAt(const std::string& filename, int col, int row);

std::shared_ptr<Fire> buildEntityAt(const std::string& filename, int col, int row);

(not real C++ code)

Search in Games

106 of 121

Template metaprogramming

The idea of template metaprogramming is to use this automatic feature of the compiler to perform certain computations at compile time instead of at runtime.

Example:

106

long factorial(int n)

{

if (n == 0)

return 1;

else

return(n * factorial(n-1));

}

int main()

{

cout << factorial(15) << endl;

return 0;

}

Calling factorial(15) computes the factorial of 15 at runtime.

template <long N>

struct Factorial

{

enum { value = N * Factorial<N - 1>::value };

};

template <>

struct Factorial<0>

{

enum { value = 1 };

};

int main()

{

cout << Factorial<15>::value << endl;

return 0;

}

factorial<15>::value is computed at compile time.

Search in Games

107 of 121

C++

Move Semantics

108 of 121

r-value and l-value

r- and l- values are properties of expressions. Broadly speaking:

  • An l-value is a function or an object.
    • They have an associated memory address and a name.
    • They’re normally on the left-hand side of an expression
    • They can be modifiable or not (const).
  • An r-value is… anything else.
    • Examples are: literals (5), temporary values, anonymous objects (with no name).
    • They have an expression scope: they run out of scope right after the expression (they cannot be assigned to).
  • Examples:

108

//l-values <-----> r-values

int func() { return 0;}

Vector2d vec (1.0f, 2.0f);

int x = 5;

int y = x + 1;

std::shared_ptr<int> intPtr = new int(10);

Search in Games

109 of 121

r-value and l-value references

An l-value reference uses the symbol & (as seen before).

An r-value reference uses a double symbol: &&.

r-value references can capture an r-value, extend its lifespan and allow modifications to it.

This is not really very exciting, and it doesn’t seem to be worth the effort…

… but they are crucial to understand move semantics.

109

int&& rref = 5; // r-value reference initialized with r-value 5

std::cout << "Val: " << rref << std::endl; // Prints 5

auto&& vecRRef = Vector2d(0.0f, 1.0f); // r-value reference initialized with an anon. object

vecRRef.setX(-1.0f); // r-value references can be modified.

std::cout << "Vec x: " << vecRRef.toString() << std::endl; // Prints [-1, 1]

Search in Games

110 of 121

Move vs Copy semantics

The following is an example of a class Entity that holds a pointer to a Component object.

This class implements a copy constructor and a copy assignment operator, among other things:

110

class Entity{

private:

Component* comp;

public:

Entity(Component* c = nullptr) : comp (c) {}

~Entity() { delete comp; }

//Copy constructor: (deep) copies content

Entity(const Entity& entity) {

std::cout << “[Copy Constructor]\n”;

comp = new Component();

*comp = *entity.comp;

}

// continues Entity class on the right

//Copy assignment: (deep) copies content

Entity& operator=(const Entity& other) {

std::cout << “[Copy Assignment]\n”;

if(&other == this) return *this;

delete comp;

comp = new Component();

*comp = other.comp;

return *this;

}

}

class Component{

public:

Component() { std::cout << "Component created\n“; }

~Component() { std::cout << "Component destroyed\n"; }

}

Search in Games

111 of 121

Move vs Copy semantics

When we execute the code above with this main:

  • In generateEntity(), “ent” is created and initialized with a dynamically allocated Component (1).
  • “ent” is returned by value (as it should, it’s a local variable), using the copy constructor, which creates another component (2).
  • When “ent” goes out of scope, it gets destroyed, liberating the component (see Entity destructor) (3).
  • The copy assignment generates another resource when assigning a value to “t” (4).
  • The temporary object (rvalue in the assignment “t = …) is destroyed when the assignment instruction ends (5).
  • “t” goes out of scope at the end of the main function, which destroys the object (6).

111

Entity generateEntity(){

Entity ent(new Component());

return ent;

}

int main(){

Entity t;

t = generateEntity();

return 0;

}

The output is:

Component created

[Copy constructor]

Component created

Component destroyed

[Copy assignment]

Component created

Component destroyed

Component destroyed

invoked by

Such simple code generates so many resource creation and destruction!

(1)

(2)

(3)

(4)

(5)

(6)

Search in Games

112 of 121

Move vs Copy semantics

Now let’s add move semantics: a move constructor and a move assignment operator:

112

class Entity{

// ... Default constructor, Destructor and

// Copy constructor as before ...

//Move constructor: moves the content

Entity(Entity&& entity) : comp(entity.comp) {

std::cout << “[Move Constructor]\n”;

entity.comp = nullptr;

}

//Move assignment operator: moves the content.

Entity& operator=(Entity&& entity){

std::cout << “[Move Constructor]\n”;

if(&entity == this) return *this;

delete comp;

comp = entity.comp;

entity.comp = nullptr;

}

}

Note the differences to the copy constructor and the assignment operator:

  • const is removed from the function argument.
  • rather than a reference to Entity, they receive an r-value reference to Entity.
  • we assign the null pointer to the Entity received by parameter.

Search in Games

113 of 121

Move vs Copy semantics

When we execute the exact same code from the main function:

  • In generateEntity(), “ent” is created and initialized with a dynamically allocated Component (1).
  • “ent” is returned by value (as it should, it’s a local variable), but this time using the move constructor, which doesn’t create any new component (2).
  • When “ent” goes out of scope, it gets destroyed, but it doesn’t manage any component that needs deletion (it was set to nullptr).
  • The temporary object is moved-assigned to “t” without any new components being created or released (3).
  • The temporary object (rvalue in the assignment “t = …) is destroyed but, as before, it manages no component that needs deletion.
  • “t” goes out of scope at the end of the main function, which destroys the object (4).

113

Entity generateEntity(){

Entity ent(new Component());

return ent;

}

int main(){

Entity t;

t = generateEntity();

return 0;

}

The output is:

Component created

[Move constructor]

[Move assignment]

Component destroyed

invoked by

With the same program flow, we obtain way fewer resource creation and destruction operations.

(1)

(2)

(3)

(4)

Search in Games

114 of 121

Move vs Copy semantics

The move constructor and the move assignment operator are called:

  1. When they are defined. The compiler will not create default move constructors and move assignment operators that move (instead of copy) class members for you. If you want to use them, you’ll need to define them explicitly.
  2. When the argument for construction or assignment is an r-value (typically a literal or a temporary value) or an l-value that is returned from a function.

114

Entity generateEntity(){

Entity ent(new Component());

return ent;

}

int main(){

Entity t;

t = generateEntity();

return 0;

}

The variable ent is an l-value returned from a function.

The right-hand side of this expression is an r-value.

Defining move constructor/operators allows:

  1. the compiler to wire the appropriate calls; and
  2. us to avoid making unnecessary copies.

Search in Games

115 of 121

Move vs Copy semantics

Move functions should always leave both objects in a well-defined state. In our example:

  • Entity&& entity will, at some point, run out of its original scope. At that time, it will be destroyed, and the member comp will be deleted.
  • But we don’t want comp to be deleted, as it’s now pointed by this new object. Our local object now needs it. Setting it to nullptr makes sure the resource entity.comp points at won’t be dereferenced.
  • If it were to be deleted, this->comp would be a dangling pointer!

115

//Move assignment operator: moves the content.

Entity& operator=(Entity&& entity){

std::cout << “[Move Constructor]\n”;

if(&entity == this) return *this;

delete comp;

comp = entity.comp;

entity.comp = nullptr;

}

Why do we do this assignment to nullptr?

Search in Games

116 of 121

Move vs Copy semantics

We can even be stricter with our Entity class and completely disable copying. We’d simply change our copy constructor and assignment operators to look like this:

With this modification, our Entity class actually behaves very similar to std:unique_ptr, but it’s a bit hard to see. For the full implementation (class is named Auto_ptr), and also an additional example of move vs copy semantics, see:

https://www.learncpp.com/cpp-tutorial/move-constructors-and-move-assignment/

116

// Copy constructor (deleted).

Entity(const Entity& entity) = delete;

//Copy assignment operator.

Entity& operator=(const Entity& other) = delete;

Note that the compiler would complain if we were to use objects of this class in a way that a copy operator is required.

Search in Games

117 of 121

Move semantics

Move semantics makes it possible for compiler to replace expensive copying operations with less expensive moves.

std::move is a function template that casts its only argument (an l-value) to an r-value.

  • std::move doesn’t move anything (!!)
  • It uses static_cast<> internally to turn an l-value into an r-value
  • The cast variable will invoke the move constructor/operator rather than its copy counterparts.

Example (a classical swap function):

117

template<class T>

void swap(T& a, T&b)

{

T tmp {a}; //Invokes copy constructor

a = b; //Invokes copy operator

b = tmp; //Invokes copy operator

}

template<class T>

void swap(T& a, T&b)

{

T tmp {std::move(a)}; //Invokes move constructor

a = std::move(b); //Invokes move operator

b = std::move(tmp); //Invokes move operator

}

  1. std::move casts the argument to an r-value
  2. the move constructor/operator is called using the r-value

Search in Games

118 of 121

Move semantics and smart pointers

std::unique_ptr uses internally move semantics: its copy constructor/assignment operator are disabled.

Therefore, this is not possible:

118

std::unique_ptr<Component> comp1{ new Component };

std::unique_ptr<Component> comp2;

std::cout << "comp1 is " << (static_cast<bool>(comp1) ? "not null\n" : "null\n");

std::cout << "comp2 is " << (static_cast<bool>(comp2) ? "not null\n" : "null\n");

comp2 = comp1; //compilation error: copy assignment is disabled.

std::cout << "comp1 is " << (static_cast<bool>(comp1) ? "not null\n" : "null\n");

std::cout << "comp2 is " << (static_cast<bool>(comp2) ? "not null\n" : "null\n");

use std::move

std::unique_ptr<Component> comp1{ new Component };

std::unique_ptr<Component> comp2;

// ...

comp2 = std::move(comp1); //calls move assignment.

// ...

What does this print?

Component created

comp1 is not null

comp2 is null

comp1 is null

comp2 is not null

Component destroyed

class Component{

public:

Component() { std::cout << "Component created\n“; }

~Component() { std::cout << "Component destroyed\n"; }

}

Search in Games

119 of 121

Move semantics and smart pointers

A few extra considerations:

  • std::unique_ptr should be returned from a function by value (not a pointer, not a reference).
  • and passed to functions as a raw pointer

119

std::unique_ptr<Component> createComponent(){

return std::make_unique<Component>();

}

void doStuff(Component* c){

// c-> ...

}

int main(){

std::unique_ptr<Component> comp = createComponent();

doStuff(comp);

return 0;

}

  • A std:unique_ptr can be converted into a std::shared_ptr via an std::shared_ptr constructor that accepts an std::unique_ptr r-value.

  • The opposite (converting an std::shared_ptr to an std::unique_ptr) is not possible.

std::unique_ptr<Vector2d> v1 = std::make_unique<Vector2d>(); //Creates a unique pointer

std::shared_ptr<Vector2d> nowShared( std::move(v1) ); //Casts to r-value and use shared pointer constructor

Search in Games

120 of 121

Resources

Books:

  • Effective C++, Third Edition (Scott Meyers, 2005)
  • Effective Modern C++ (Scott Meyers, 2014)
  • A Tour C++, Second Edition (Bjarne Stroustrup, 2019)

Online Resources:

120

Search in Games

121 of 121