ECS7014P – Advanced Game Development
C++
Lecturer: Diego Perez Liebana
School of Electronic Engineering and Computer Science
C++
Overview
Introduction
C++ is… a compiled language:
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
Introduction
C++ is… a federation of languages:
C++ is…
4
Search in Games
A note on Compilers and Versions
There are multiple C++ compilers. The ones used more often are:
Also, C++ has different versions (C++2.0, C++ 98, C++03, C++11, C++14, C++17, C++20)
And there exist multiple Integrated Development Environments (IDE) that support programming in C++:
5
Search in Games
A note on Compilers and Versions
During this course, we will use:
More information:
6
Search in Games
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
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
C++
Declarations and Definitions
Declarations
A declaration is a statement that introduces a name into a scope:
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
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:
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
Definitions
A definition specifies exactly what a name refers to.
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
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
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
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
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
C++
Types I: basic types
Types and Variables: definition
All names and expressions have a type (i.e. functions).
Some useful definitions:
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
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
Types and Variables: arrays
An array allows to hold a contiguously allocated sequence of elements of the same type in memory.
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
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
Types and variables: initialization
There are 4 basic ways to initialize (give a value) to a variable in C++:
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!
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
Types and variables: initialization
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.
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
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++:
C++ Style casts:
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
Types and Variables: casting
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
C++
Types II: auto, scope and const
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
Types and Variables: auto
Advantages of using auto:
Disadvantages:
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
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
Types and Variables: scope
A declaration of an object introduces its name into a scope:
Objects must be initialized before they are used and will be destroyed at the end of their scope.
30
Search in Games
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
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
Types and Variables: const
C++ allows you to define the mutability of variables in their scope:
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
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
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
Types and Variables: constexpr
constexpr represents an expression that must be evaluated at compile time.
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
C++
Utils
Input/Output
The Input/Output (I/O) stream library provides formatted and unformatted ways to read and write text and numeric values:
Similar to standard input/output, other streams define I/O for strings and files:
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
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
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.
40
Source files
Object files
Executable file
Compiler
Linker
Libraries
.lib / .dll
Preprocessor
Compiler
Search in Games
The Preprocessor Directives
Apart from #include, other common preprocessor directives are:
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
The Preprocessor Directives
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
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.
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
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
C++
Memory: Raw pointers
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:
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
Memory Allocation
Types of memory allocation:
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
Memory Allocation
A pointer holds the address of an object of the appropriate type.
48
void func() {
}
Stack Memory | (mem address) |
| 0x0010 |
| 0x0011 |
| 0x0012 |
| 0x0013 |
Heap Memory | (mem address) |
| 0x0A12 |
| 0x0A13 |
| 0x0A14 |
| 0x0A15 |
Search in Games
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.
Search in Games
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.
Search in Games
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.
Search in Games
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.
Search in Games
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??
It’s a memory leak.
Search in Games
Memory Allocation
When we are done with dynamically allocated memory, we need to release (deallocate) it.
This is done via de delete operator.
Dangling pointers:
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
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
C++
Memory: References
References
By default, C++ passes objects to and from function by value.
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:
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
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
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
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
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
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
C++
Memory: Smart Pointers
Memory Allocation: Smart Pointers
Traditional (raw) pointers have many drawbacks:
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
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:
65
Search in Games
Memory Allocation: Smart Pointers
Example:
How to access the managed resource from a smart pointer?
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
Memory Allocation: Smart Pointers
Common pitfalls in std::unique_ptr and 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
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
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
C++
Object-Oriented Principles:
User defined types, constructors and destructors
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
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
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
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).
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
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.
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
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:
Search in Games
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
Constructors and Destructors
These functions are called automatically when objects of the class are created, copied or destroyed:
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
C++
Object-Oriented Principles:
Inheritance, visibility and polymorphism
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
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
Object-Oriented Principles: construction
When a derived class is instantiated, the following happens in order:
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
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.
Search in Games
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
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
Object-Oriented Principles: polymorphism
Polymorphism: A virtual function is a type of function that resolves to the most-derived function in a hierarchy.
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
Object-Oriented Principles: virtual functions
Notes on virtual functions:
86
Constructors of a base class are called before constructors of the derived class.
Destructors of a derived class are called before destructors of a base class (opposite order to constructors).
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
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
C++
Object-Oriented Principles:
Forward declarations and function pointers
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
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:
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
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
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:
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
Forward Declarations
In some cases, functions (or classes) need to be forward declared so the compiler can validate the code in a single file.
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
//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
C++
The Standard (Template) Library
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:
Associative containers: elements are automatically sorted when inserted in the structure:
95
Search in Games
The Standard (Template) Library
Container Adapters: containers adapted for specific uses.
Also in the standard library:
96
Search in Games
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:
97
#include <vector>
// ...
std::vector<int>::iterator vectorIterator;
Search in Games
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
Iterators
Iterators also provide a set of basic functions:
Therefore, iterators can be of two types:
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
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
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
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.
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
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
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:
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
Template metaprogramming
How do templates really work?
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
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
C++
Move Semantics
r-value and l-value
r- and l- values are properties of expressions. Broadly speaking:
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
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
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
Move vs Copy semantics
When we execute the code above with this main:
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
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:
Search in Games
Move vs Copy semantics
When we execute the exact same code from the main function:
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
Move vs Copy semantics
The move constructor and the move assignment operator are called:
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:
Search in Games
Move vs Copy semantics
Move functions should always leave both objects in a well-defined state. In our example:
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
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
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.
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
}
Search in Games
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
Move semantics and smart pointers
A few extra considerations:
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;
}
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
Resources
Books:
Online Resources:
120
Search in Games