C++ Lecture Organized
C++ Lecture Organized
Organization
2
Systems Programming in C++
Lecture
Course Goals
3
Organization
Assignments
6
Organization
Practical Prerequisites
Practical prerequisites
• No previous experience with C or C++ required
• Familiarity with another general-purpose programming language
Operating System
• Working Linux operating system (e.g. recent Ubuntu)
• Ideally with root access
• Basic experience with Linux (in particular with shell)
• You are free to use your favorite OS, we only support Linux
• Our CI server runs Linux
• It will run automated tests on your submissions
4
Organization
Grading
Grading system
• Weekly assignments: Varying number of points depending on workload
7
Organization
Literature
Primary
• C++ Reference Documentation. (https://en.cppreference.com/)
• Lippman, 2013. C++ Primer (5th edition). Only covers C++11.
• Stroustrup, 2013. The C++ Programming Language (4th edition). Only
covers C++11.
• Meyers, 2015. Effective Modern C++. 42 specific ways to improve your use
of C++11 and C++14..
Supplementary
• Aho, Lam, Sethi & Ullman, 2007. Compilers. Principles, Techniques & Tools
(2nd edition).
• Tanenbaum, 2006. Structured Computer Organization (5th edition).
8
Introduction
Introduction
10
Introduction
What is C++?
Key characteristics
• Compiled language
• Statically typed language
• Facilities for low-level programming
11
Introduction
Initial development
• Bjarne Stroustrup at Bell Labs (since 1979)
• In large parts based on C
• Inspirations from Simula67 (classes) and Algol68 (operator overloading)
12
Introduction
Performance
• Flexible level of abstraction (very low-level to very high-level)
• High-performance even for user-defined types
• Direct mapping of hardware capabilities
• Zero-overhead rule: “What you don’t use, you don’t pay for.” (Bjarne
Stroustrup)
Flexibility
• Choose suitable programming paradigm
• Comprehensive ecosystem (tool chains & libraries)
• Scales easily to very large systems (with some discipline)
• Interoperability with other programming languages (especially C)
13
Background
Background
14
Background Central Processing Unit
15
Background Central Processing Unit
Control
Unit
Arithmetic
Logical Unit
(ALU)
Registers
Bus
16
Background Central Processing Unit
Components of a CPU
Control Unit
• Fetch instructions from memory and determine their type
• Orchestrate other components
Registers
• Small, high-speed memory with fixed size and function
• Temporary results and control information (one number / register)
• Program Counter (PC): Next instruction to be fetched
• Instruction Register (IR): Instruction currently being executed
17
Background Central Processing Unit
A+B
A Registers
B
ALU
18
Background Central Processing Unit
19
Background Central Processing Unit
Instruction Categories
Register-register instructions
• Fetch two operands from registers into ALU input registers
• Perform some computation on values
• Store result back into one of the registers
• Low latency, high throughput
Register-memory instructions
• Fetch memory words into registers
• Store registers into memory words
• Potentially incur high latency and stall the CPU
20
Background Central Processing Unit
Fetch-Decode-Execute Cycle
21
Background Central Processing Unit
22
Background Central Processing Unit
23
Background Central Processing Unit
Instruction-Level Parallelism
Instruction Prefetching
• Fetch instructions from memory in advance
• Hold prefetched instructions in buffer for fast access
• Breaks instruction execution into fetching and actual execution
Pipelining
• Divide instruction execution into many steps
• Each step handled in parallel by dedicated piece of hardware
• Central to modern CPUs
24
Background Central Processing Unit
Pipelining (1)
S1 S2 S3 S4 S5
Instruction Instruction Operand Instruction Write
Fetch Decode Fetch Execution Back
Frontend Backend
S1 1 2 3 4 5
S2 1 2 3 4
S3 1 2 3
S4 1 2
S5 1
Time 1 2 3 4 5
25
Background Central Processing Unit
Pipelining (2)
CPU front-end
• Fetch instructions from memory
• Decode assembly instructions to microoperations
• Provide stream of work to pipeline back-end (Skylake: 6 µops / cycle)
• Requires branch prediction (implemented in special hardware)
CPU back-end
• Execute microoperations out-of-order as soon as possible
• Complex bookkeeping required
• Microoperations are run on execution units (e.g. ALU, FPU)
26
Background Central Processing Unit
Superscalar Architectures
Superscalar architectures
• Fetch stage is typically much faster than execution
• Issue multiple instructions per clock cycle in a single pipeline
• Replicate (some) execution units in execution stage to keep up with fetch
stage
27
Background Central Processing Unit
28
Background Central Processing Unit
Multiprocessors
29
Background Primary Memory
Main Memory
30
Background Primary Memory
31
Background Primary Memory
Address 0000 48 65 6c 6c 6f 20 57 6f
Hexadecimal 0008 72 6c 64 21 20 49 20 6c
0010 69 6b 65 20 43 2b 2b 21
Address
00 01 02 03 04 05 06 07
0000 H e l l o W o
Address
ASCII 0008 r l d ! I l
0010 i k e C + + !
32
Background Primary Memory
33
Background Primary Memory
00 01 02 03 00 01 02 03
00 00 00 2a 00 2a 00 00
34
Background Primary Memory
35
Background Primary Memory
36
Background Primary Memory
Introduce small but fast cache between CPU and main memory
• CPU transparently keeps recently accessed data in cache (temporal locality)
• Memory is divided into blocks (cache lines)
• Whenever a memory cell is referenced, load the entire corresponding cache
line into the cache (spatial locality)
• Requires specialized eviction strategy
Intel CPUs
• 3 caches (L1, L2, L3) with increasing size and latency
• Caches are inclusive (i.e. L1 is replicated within L2, and L2 within L3)
• 64 byte cache lines
37
Background Primary Memory
CPU 1 CPU 2
Main
Memory
L1-I L1-D L1-I L1-D
Unified L2 Unified L2
Unified L3
38
Background Primary Memory
39
Background Primary Memory
40
Background Assembly
41
Background Assembly
Example
42
Background Assembly
Registers (1)
43
Background Assembly
Registers (2)
Important registers on x86-64
44
Background Assembly
45
Introduction to the C++ Ecosystem
46
Introduction to the C++ Ecosystem Hello World
myprogram.cpp
#include <iostream>
int main(int argc, char** argv) {
std::cout << "Hello " << argv[1] << '!' << std::endl;
return 0;
}
47
Introduction to the C++ Ecosystem Compiler
• Programs that transform C++ files into executables are called compilers
• Popular compilers: gcc (GNU), clang (llvm)
• Minimal example to compile the hello world program with gcc:
48
Introduction to the C++ Ecosystem Compiler
Compiler Flags
49
Introduction to the C++ Ecosystem make
make
• C++ projects usually consist of many .cpp (implementation files) and .hpp
(header files) files
• Each implementation file needs to be compiled into an object file first, then
all object files must be linked
• Very repetitive to do this by hand
• When one .cpp file changes, only the corresponding object file should be
recompiled, not all
• When one .hpp file changes, only implementation files that use it should be
recompiled
• make is a program that can automate this
• Requires a Makefile
• GNU make manual:
https://www.gnu.org/software/make/manual/make.html
50
Introduction to the C++ Ecosystem make
Basic Makefile
• Makefiles consist of rules and contain variables
• Each rule has a target, prerequisites, and a recipe
• Recipes are only executed when the prerequisites are newer than the target or
when the target does not exist
• Note: The indentation in Makefiles must be exactly one tab character, no
spaces!
Makefile
CONTENT="test 123" # set the variable CONTENT
# rule and recipe to generate the target file foo
foo:
echo $(CONTENT) > foo
# $^ always contains all prerequisites ("foo baz" here)
# $< contains only the first prerequisite ("foo" here)
# $@ contains the target ("bar" here)
bar: foo baz
cat $^ > $@
51
Introduction to the C++ Ecosystem make
52
Introduction to the C++ Ecosystem make
Advanced Makefile
• Recipes are usually the same for most files
• Pattern rules can be used to reuse a recipe for multiple files
Makefile
CXX?=g++ # set CXX variable only if it's not set
CXXFLAGS+= -O3 -Wall -Wextra # append to CXXFLAGS
SOURCES=foo.cpp bar.cpp
%.o: %.cpp # pattern rule to make .o files out of .cpp files
$(CXX) $(CXXFLAGS) -c -o $@ $<
# use a substitution reference to get .o file names
myprogram: myprogram.o $(SOURCES:.cpp=.o)
$(CXX) $(CXXFLAGS) -o $@ $^
53
Introduction to the C++ Ecosystem CMake
CMake
54
Introduction to the C++ Ecosystem CMake
Basic CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(myprogram)
set(MYPROGRAM_FILES sayhello.cpp saybye.cpp)
add_executable(myprogram myprogram.cpp ${MYPROGRAM_FILES})
55
Introduction to the C++ Ecosystem CMake
CMake Commands
cmake_minimum_required(VERSION 3.10)
Require a specific cmake version.
project(myproject)
Define a C/C++ project with the name “myproject”, required for every project.
set(FOO a b c)
Set the variable ${FOO} to be equal to a;b;c (list).
add_executable(myprogram a.cpp b.cpp)
Define an executable to be built that consists of the source files a.cpp and
b.cpp.
add_library(mylib a.cpp b.cpp)
Similar to add_executable() but build a library.
add_compile_options(-Wall -Wextra)
Add -Wall -Wextra to all invocations of the compiler.
target_link_library(myprogram mylib)
Link the executable or library myprogram with the library mylib.
56
Introduction to the C++ Ecosystem CMake
CMake Variables
CMake has many variables that influence how the executables and libraries are
built. They can be set in the CMakeLists.txt with set(), on the command
line with cmake -D FOO=bar, or with the program ccmake.
CMAKE_CXX_STANDARD=20
Set the C++ to standard to C++20, effectively adds -std=c++20 to the compiler
flags.
CMAKE_CXX_COMPILER=clang++
Set the C++ compiler to clang++.
CMAKE_BUILD_TYPE=Debug
Set the “build type” to Debug. Other possible values: Release,
RelWithDebInfo. This mainly affects the optimization compiler flags.
CMAKE_CXX_FLAGS(_DEBUG/_RELEASE)=-march=native
Add -march=native to all compiler invocations (or only for the Debug or
Release build types).
57
Introduction to the C++ Ecosystem CMake
58
Introduction to the C++ Ecosystem CMake
cmake_example_project
├── CMakeLists.txt
├── lib • This project contains the library
│ ├── CMakeLists.txt greetings and the executable
│ ├── saybye.cpp print_greetings
│ ├── saybye.hpp • The library consists of the files
│ ├── sayhello.cpp sayhello.cpp and saybye.cpp
│ └── sayhello.hpp • You can find this project in our
└── src Gitlab
├── CMakeLists.txt
└── print_greetings.cpp
59
Introduction to the C++ Ecosystem Git
v1 v2 v3 v6
v4 v5
Added file C Changed file C
60
Introduction to the C++ Ecosystem Git
Git
• Many VCS exist, Git is a very popular one: Used by Linux, GCC, LLVM, etc.
• Git in particular has the following advantages compared to other version
control systems (VCS):
• Open source (LGPLv2.1)
• Decentralized, i.e., no server required
• Efficient management of branches and tags
• All Git commands are document with man-pages (e.g. type man
git-commit to see documentation for the command git commit)
• Pro Git book: https://git-scm.com/book
• Git Reference Manual: https://git-scm.com/docs
61
Introduction to the C++ Ecosystem Git
Git Concepts
62
Introduction to the C++ Ecosystem Git
No commits yet
63
Introduction to the C++ Ecosystem Git
When working with a Git repository, changes can live in any of the following
places:
• In the working directory (when you edit a file)
• In the staging area (when you use git add)
• In a commit (after a git commit)
Once a change is in a commit and it is referenced by at least one branch or tag,
you can always restore it even if you remove the file.
working directory staging area commit
git add
git commit
git checkout
git reset
64
Introduction to the C++ Ecosystem Git
Committing Changes
65
Introduction to the C++ Ecosystem Git
66
Introduction to the C++ Ecosystem Git
git diff
View the changes in the working directory (without the staging area).
git diff --staged
View the changes in the staging area (without the working directory).
git diff HEAD
View the changes in the working directory and the staging area.
git diff branch1..branch2
View the changes between two branches (or tags, commits).
Example output of git diff
diff --git a/foo b/foo
index e965047..980a0d5 100644
--- a/foo
+++ b/foo
@@ -1 +1 @@
-Hello
+Hello World!
67
Introduction to the C++ Ecosystem Git
git branch
Show all branches and which one is active.
git branch <name>
Create a new branch that points to the current commit (HEAD).
git checkout <name>
Switch to another branch, i.e. change all files in the working directory so that
they are equal to the tree of the other branch.
git checkout -b <name>
Create a branch and switch to it.
git tag
Show all tags.
git tag [-s] <name>
Create a new tag that points to the current commit. Is signed with PGP when
-s is given.
68
Introduction to the C++ Ecosystem Git
master
C1 C2 C3
C4 C5
feature-abc
C1 C2 C3 Cm C1 C2 C3 C 40 C50
C4 C5
69
Introduction to the C++ Ecosystem Git
70
Introduction to the C++ Ecosystem Git
71
Introduction to the C++ Ecosystem Git
72
Introduction to the C++ Ecosystem Git
73
Introduction to the C++ Ecosystem Git
• Sometimes, especially when reading a new code base, you want to know
which commit changed a certain line
• Also, sometimes you want to know who wrote a certain line
git blame <filename>
• Shows the given file with commit annotations
• Each line starts with the commit hash, the name of the author, and the
commit date
tig blame <filename>
• Like git blame but with a nicer interface
• Allows to “re-blame” at a given line, i.e. showing the selected line in the
version just before it was last modified
• tig can also be used with other git commands: tig log, tig diff, etc.
74
Introduction to the C++ Ecosystem Git
.gitignore
• git status, git diff, etc. usually look at all files in all subdirectories of
the repository
• If files or directories should always be excluded (e.g. build or cache
directories), they can be added to the .gitignore file
• This file contains one entry per line, lines starting with # are skipped:
foo.txt Ignores all files named foo.txt
/foo.txt Ignores only the file foo.txt in the top-level directory
foo/ Ignores all directories named foo and their contents
*f* Ignores all files and directories that contain the letter f
.git
• This directory contains all commits, branches, etc.
• E.g., .git/refs/heads contains one file per branch
• If you remove this directory, all data is lost!
75
Basic C++ Syntax
76
Basic C++ Syntax
Overview
77
Basic C++ Syntax
Look at these links and familiarize yourself with the reference documentation!
78
Basic C++ Syntax Comments
Comments
Example
79
Basic C++ Syntax Basic Types and Variables
Fundamental Types
All other types are composed of these fundamental types in some way
80
Basic C++ Syntax Basic Types and Variables
Void Type
81
Basic C++ Syntax Basic Types and Variables
Boolean Type
82
Basic C++ Syntax Basic Types and Variables
Signedness modifiers
• signed integers will have signed representation (i.e. they can represent
negative numbers)
• Since C++20 signed integers must use two’s complement representation
• unsigned integers will have unsigned representation (i.e. they can only
represent non-negative numbers)
Size modifiers
• short integers will be optimized for space (at least 16 bits wide)
• long integers will be at least 32 bits wide
• long long integers will be at least 64 bits wide
83
Basic C++ Syntax Basic Types and Variables
By default integers are signed, thus the signed keyword can be omitted
// e and f have the same type
signed int e;
int f;
85
Basic C++ Syntax Basic Types and Variables
#include <cstdint>
86
Basic C++ Syntax Basic Types and Variables
87
Basic C++ Syntax Basic Types and Variables
Character Types
Character types represent character codes and (to some extent) integral values
• Identified by C++ keywords signed char and unsigned char
• Minimum width is 8 bit, large enough to represent UTF-8 eight-bit code units
• The C++ type char may either be equivalent to signed char or
unsigned char, depending on the implementation
• Nevertheless char is always a distinct type
• signed char and unsigned char are sometimes used to represent small
integral values
88
Basic C++ Syntax Basic Types and Variables
89
Basic C++ Syntax Basic Types and Variables
90
Basic C++ Syntax Basic Types and Variables
Example
uint16_t i = 257;
uint8_t j = i; // j is 1
if (j) {
/* executed if j is not zero */
}
91
Basic C++ Syntax Basic Types and Variables
92
Basic C++ Syntax Basic Types and Variables
Example
foo.cpp foo.o
int foo(int i) { foo(int):
if ((i + 1) > i) movl $42, %eax
return 42; retq
return 123;
}
93
Basic C++ Syntax Basic Types and Variables
Example
• Out-of-bounds array accesses are undefined behavior
• Therefore, the compiler does not need to generate range checks for each
array access
94
Basic C++ Syntax Basic Types and Variables
Variables
void foo() {
unsigned i = 0, j;
unsigned meaningOfLife = 42;
}
95
Basic C++ Syntax Basic Types and Variables
Important differences
• Options 1 and 2 simply assign the value of the expression to the variable,
possibly invoking implicit type conversions
• Option 3 results in a compile error if implicit type conversions potentially
result in loss of information
96
Basic C++ Syntax Basic Types and Variables
double a = 3.1415926;
double b(42);
unsigned c = a; // OK: c == 3
unsigned d(b); // OK: d == 42
unsigned e{a}; // ERROR: potential information loss
unsigned f{b}; // ERROR: potential information loss
97
Basic C++ Syntax Basic Types and Variables
Integer Literals
Integer literals represent constant values embedded in the source code
• Decimal: 42
• Octal: 052
• Hexadecimal: 0x2a
• Binary: 0b101010
98
Basic C++ Syntax Basic Types and Variables
Floating-point literals
One of the following suffixes may be appended to a literal to specify its type
• float suffix: 1.0f or 1.0F
• long double suffix: 1.0l or 1.0L
99
Basic C++ Syntax Basic Types and Variables
Character Literals
One of the following prefixes may be prepended to a literal to specify its type
• UTF-8 prefix: u8'a', u8'b'
• UTF-16 prefix: u'a', u'b'
• UTF-32 prefix: U'a', U'b'
100
Basic C++ Syntax Basic Types and Variables
Any type T in C++ (except function and reference types) can be cv-qualified
• const-qualified: const T
• volatile-qualified: volatile T
• cv-qualifiers can appear in any order, before or after the type
Semantics
• const objects cannot be modified
• Any read or write access to a volatile object is treated as a visible side
effect for the purposes of optimization
• volatile should be avoided in most cases (it is likely to be deprecated in
future versions of C++)
• Use atomics instead
101
Basic C++ Syntax Basic Types and Variables
int main() {
int a = 1; // will be optimized out
int b = 2; // will be optimized out
volatile int c = 42;
volatile int d = c + b;
}
main:
movl $42, -4(%rsp) # volatile int c = 42
movl -4(%rsp), %eax # volatile int d = c + b;
addl $2, %eax # volatile int d = c + b;
movl %eax, -8(%rsp) # volatile int d = c + b;
movl $0, %eax # implicit return 0;
ret
102
Basic C++ Syntax Expressions
Expression Fundamentals
Fundamental expressions
• Variable names
• Literals
103
Basic C++ Syntax Expressions
Value Categories
Broadly (and inaccurately) there are two value categories: lvalues and rvalues
• lvalues refer to the identity of an object
• rvalues refer to the value of an object
• Modifiable lvalues can appear on the left-hand side of an assignment
• lvalues and rvalues can appear on the right-hand side of an assignment
104
Basic C++ Syntax Expressions
Operator Explanation
+a Unary plus
-a Unary minus
a + b Addition
a - b Subtraction
a * b Multiplication
a / b Division
a % b Modulo
~a Bitwise NOT
a & b Bitwise AND
a | b Bitwise OR
a ^ b Bitwise XOR
a << b Bitwise left shift
a >> b Bitwise right shift
105
Basic C++ Syntax Expressions
Incorrectly using the arithmetic operators can lead to undefined behavior, e.g.
• Signed overflow (see above)
• Division by zero
• Shift by a negative offset
• Shift by an offset larger than the width of the type
106
Basic C++ Syntax Expressions
Operator Explanation
!a Logical NOT
a && b Logical AND (short-circuiting)
a || b Logical OR (short-circuiting)
a == b Equal to
a != b Not equal to
a < b Less than
a > b Greater than
a <= b Less than or equal to
a >= b Greater than or equal to
a <=> b Three-way comparison
Most C++ logical and relational operators have the usual semantics
107
Basic C++ Syntax Expressions
108
Basic C++ Syntax Expressions
Operator Explanation
a = b Simple assignment
a += b Addition assignment
a -= b Subtraction assignment
a *= b Multiplication assignment
a /= b Division assignment
a %= b Modulo assignment
a &= b Bitwise AND assignment
a |= b Bitwise OR assignment
a ^= b Bitwise XOR assignment
a <<= b Bitwise left shift assignment
a >>= b Bitwise right shift assignment
Notes
• The left-hand side of an assignment operator must be a modifiable lvalue
• For built-in types a OP= b is equivalent to a = a OP b except that a is
only evaluated once
109
Basic C++ Syntax Expressions
unsigned a, b, c;
a = b = c = 42; // a, b, and c have value 42
unsigned d;
if (d = computeValue()) {
// executed if d is not zero
} else {
// executed if d is zero
}
110
Basic C++ Syntax Expressions
Operator Explanation
++a Prefix increment
--a Prefix decrement
a++ Postfix increment
a-- Postfix decrement
111
Basic C++ Syntax Expressions
Operator Explanation
a ? b : c Conditional operator
Semantics
• a is evaluated and converted to bool
• If the result was true, b is evaluated
• Otherwise c is evaluated
The type and value category of the resulting expression depend on the operands
112
Basic C++ Syntax Expressions
113
Basic C++ Syntax Expressions
114
Basic C++ Syntax Expressions
115
Basic C++ Syntax Expressions
116
Basic C++ Syntax Expressions
117
Basic C++ Syntax Statements
Simple Statements
int i = 0;
{ // start of block
int i = 0; // declaration statement
} // end of block, i goes out of scope
int i = 1; // declaration statement
118
Basic C++ Syntax Statements
Scope
int a = 21;
int b = 0;
{
int a = 1; // scope of the first a is interrupted
int c = 2;
b = a + c + 39; // a refers to the second a, b == 42
} // scope of the second a and c ends
b = a; // a refers to the first a, b == 21
b += c; // ERROR: c is not in scope
119
Basic C++ Syntax Statements
If Statement (1)
if (init-statement; condition)
then-statement
else
else-statement
Explanation
• If condition evaluates to true after conversion to bool, then-statement is
executed, otherwise else-statement is executed
• Both init-statement and the else branch can be omitted
• If present, init-statement must be an expression or declaration statement
• condition must be an expression statement or a single declaration
• then-statement and else-statement can be arbitrary (compound) statements
120
Basic C++ Syntax Statements
If Statement (2)
The init-statement form is useful for local variables only needed inside the if
Equivalent formulation
{
unsigned value = computeValue();
if (value < 42) {
// do something
} else {
// do something else
}
}
121
Basic C++ Syntax Statements
If Statement (3)
In nested if-statements, the else is associated with the closest if that does not
have an else
// INTENTIONALLY BUGGY!
if (condition0)
if (condition1)
// do something if (condition0 && condition1) == true
else
// do something if condition0 == false
// Working as intended
if (condition0) {
if (condition1)
// do something if (condition0 && condition1) == true
} else {
// do something if condition0 == false
}
122
Basic C++ Syntax Statements
Explanation
• condition may be an expression or single declaration that is convertible to an
enumeration or integral type
• The body of a switch statement may contain an arbitrary number of
case constant: labels and up to one default: label
• The constant values for all case: labels must be unique
• If condition evaluates to a value for which a case: label is present, control is
passed to the labelled statement
• Otherwise, control is passed to the statement labelled with default:
• The break; statement can be used to exit the switch
123
Basic C++ Syntax Statements
Regular example
switch (computeValue()) {
case 21:
// do something if computeValue() was 21
break;
case 42:
// do something if computeValue() was 42
break;
default:
// do something if computeValue() was != 21 and != 42
break;
}
124
Basic C++ Syntax Statements
switch (computeValue()) {
case 21:
case 42:
// do something if computeValue() was 21 or 42
break;
default:
// do something if computeValue() was != 21 and != 42
break;
}
125
Basic C++ Syntax Statements
While Loop
while (condition)
statement
Explanation
• Executes statement repeatedly until the value of condition becomes false.
The test takes place before each iteration.
• condition may be an expression that can be converted to bool or a single
declaration
• statement may be an arbitrary statement
• The break; statement may be used to exit the loop
• The continue; statement may be used to skip the remainder of the body
126
Basic C++ Syntax Statements
Do-While Loop
do
statement
while (condition);
Explanation
• Executes statement repeatedly until the value of condition becomes false.
The test takes place after each iteration.
• condition may be an expression that can be converted to bool or a single
declaration
• statement may be an arbitrary statement
• The break; statement may be used to exit the loop
• The continue; statement may be used to skip the remainder of the body
127
Basic C++ Syntax Statements
unsigned i = 42;
do {
// executed once
} while (i < 42);
128
Basic C++ Syntax Statements
Explanation
• Executes init-statement once, then executes statement and
iteration-expression repeatedly until condition becomes false
• init-statement may either be an expression or declaration
• condition may either be an expression that can be converted to bool or a
single declaration
• iteration-expression may be an arbitrary expression
• All three of the above statements may be omitted
• The break; statement may be used to exit the loop
• The continue; statement may be used to skip the remainder of the body
129
Basic C++ Syntax Statements
130
Basic C++ Syntax Functions
Functions in C++
• Associate a sequence of statements (the function body) with a name
• Functions may have zero or more function parameters
• Functions can be invoked using a function-call expression which initializes the
parameters from the provided arguments
name ( argument-list );
131
Basic C++ Syntax Functions
unsigned meaningOfLife() {
// extremely complex computation
return 42;
}
int main() {
// run the program
}
132
Basic C++ Syntax Functions
133
Basic C++ Syntax Functions
Argument Passing
unsigned square(unsigned v) {
v = v * v;
return v;
}
int main() {
unsigned v = 8;
unsigned w = square(v); // w == 64, v == 8
}
C++ differs from other programming languages (e.g. Java) in this respect
• Parameters can explicitly be passed by reference
• Essential to keep argument-passing semantics in mind, especially when
used-defined classes are involved
134
Basic C++ Syntax Functions
Default Arguments
A function definition can include default values for some of its parameters
• Indicated by including an initializer for the parameter
• After a parameter with a default value, all subsequent parameters must have
default values as well
• Parameters with default values may be omitted when invoking the function
int main() {
int x = foo(1); // x == 6
int y = foo(1, 1); // y == 5
int z = foo(1, 1, 1); // z == 3
}
135
Basic C++ Syntax Functions
136
Basic C++ Syntax Functions
Valid example
int main() {
foo(1u); // calls foo(unsigned)
foo(1.0f); // calls foo(float)
}
137
Basic C++ Syntax Basic IO
Basic IO (1)
#include <iostream>
// ----------------------------------
int main() {
unsigned i = 42;
std::cout << "The value of i is " << i << std::endl;
}
138
Basic C++ Syntax Basic IO
Basic IO (2)
#include <iostream>
// ----------------------------------
int main() {
std::cout << "Please enter a value: " << std::flush;
unsigned v;
std::cin >> v;
std::cout << "You entered " << v << std::endl;
}
139
Basic C++ Syntax Code Style
140
Basic C++ Syntax Code Style
141
Basic C++ Syntax Code Style
142
Declarations and Definitions
143
Declarations and Definitions Objects
Objects
One of the core concepts of C++ are objects.
• The main purpose of C++ programs is to interact with objects in order to
achieve some goal
• Examples of objects are local and global variables
• Examples of concepts that are not objects are functions, references, and
values
144
Declarations and Definitions Objects
145
Declarations and Definitions Objects
Lifetime
In addition to their storage duration objects also have a lifetime which is closely
related. References also have a lifetime.
• The lifetime of an object or reference starts when it was fully initialized
• The lifetime of an object ends when its destructor is called (for objects of
class types) or when its storage is deallocated or reused (for all other types)
• The lifetime of an object never exceeds its storage duration.
• The lifetime of a reference ends as if it were a “scalar” object (e.g. an int
variable)
Generally, using an object outside of its lifetime leads to undefined behavior.
Lifetime issues are the main source of memory bugs!
• A C++ compiler can only warn about very basic lifetime errors
• If the compiler warns, always fix your code so that the warning disappears
147
Declarations and Definitions Objects
Linkage
Most declarations have a (conceptual) property called linkage. This property
determines how the name of the declaration will be visible in the current and in
other translation units. There are three types of linkage:
no linkage:
• Names can only be referenced from the scope they are in
• Local variables
internal linkage:
• Names can only be referenced from the same translation unit
• Global functions and variables declared with static
• Global variables that are not declared with extern
• All declarations in namespaces without name (“anonymous namespaces”)
external linkage:
• Names can be referenced from other translation units
• Global functions (without static)
• Global variables with extern
148
Declarations and Definitions Objects
Declaration Specifiers
Some declarations can also contain additional specifiers. The following lists shows
a few common ones and their effect on storage duration and linkage. We will see
some more specifiers in future lectures.
inline Permit multiple definitions of the same function. Despite the name,
has (almost) nothing to do with the inlining optimization. See the
slides about the “One Definition Rule” for more information.
149
Declarations and Definitions Namespaces
Namespaces (1)
Larger projects may contain many names (functions, classes, etc.)
• Should be organized into logical units
• May incur name clashes
• C++ provides namespaces for this purpose
Namespace definitions
namespace identifier {
namespace-body
}
Explanation
• identifier may be a previously unused identifier, or the name of a namespace
• namespace-body may be a sequence of declarations
• A name declared inside a namespace must be qualified when accessed from
outside the namespace (:: operator)
150
Declarations and Definitions Namespaces
Namespaces (2)
Qualified name lookup
namespace A {
void foo() { /* do something */ }
void bar() {
foo(); // refers to A::foo
}
}
namespace B {
void foo() { /* do something */ }
}
int main() {
A::foo(); // qualified name lookup
B::foo(); // qualified name lookup
151
Declarations and Definitions Namespaces
Namespaces (3)
namespace A { namespace B {
void foo() { /* do something */ }
}}
// equivalent definition
namespace A::B {
void bar() {
foo(); // refers to A::B::foo
}
}
int main() {
A::B::bar();
}
152
Declarations and Definitions Namespaces
Namespaces (4)
Code can become rather confusing due to large number of braces
• Use visual separators (comments) at sensible points
• (Optionally) add comments to closing namespace braces
//----------------------------------
namespace A::B {
//----------------------------------
void foo() {
// do something
}
//----------------------------------
void bar() {
// do something else
}
//----------------------------------
} // namespace A::B
//----------------------------------
153
Declarations and Definitions Namespaces
Namespaces (5)
• Always using fully qualified names makes code easier to read
• Sometimes it is obvious from which namespace the names come from in
which case one prefers to use unqalified names
• For this using and using namespace can be used
• using namespace X imports all names from namespace X
• using X::a only imports the name a from X into the current namespace
• Should not be used in header files to not influence other implementation files
namespace A { int x; }
namespace B { int y; int z; }
using namespace A;
using B::y;
int main() {
x = 1; // Refers to A::x
y = 2; // Refers to B::y
z = 3; // ERROR: z was not declared in this scope
B::z = 3; // OK
}
154
Declarations and Definitions Declarations
Declarations
C++ code that introduces a name that can then be referred to is called
declaration. There are many different kinds of declarations:
• variable declarations: int a;
• At global scope, use extern int a;
• function declarations: void foo();
• namespace declarations: namespace A { }
• using declarations: using A::x;
• class declarations: class C;
• template declarations: template <typename T> void foo();
• ...
155
Declarations and Definitions Definitions
Definitions
When a name is declared it can be referenced by other code. However, most uses
of a name also require the name to be defined in addition to be declared.
Formally, this is called odr-use and covers the following cases:
• The value of a variable declaration is read or written
• The address of a variable or function declaration is taken
• A function is called
• An object of a class declaration is used
Most declarations are also definitions, with some exceptions such as
• Any declaration with an extern specifier and no initializer
• Function declarations without function bodies
• Declaration of a class name (“forward declaration”)
156
Declarations and Definitions Definitions
157
Declarations and Definitions Definitions
a.cpp
int i = 5; // OK: declares and defines i
int i = 6; // ERROR: redefinition of i
158
Declarations and Definitions Definitions
159
Declarations and Definitions Definitions
160
Declarations and Definitions Header and Implementation Files
When distributing code over several files it is usually split into header and
implementation files
• Header and implementation files have the same name, but different suffixes
(e.g. .hpp for headers, .cpp for implementation files)
• Header files contain only declarations that should be visible and usable in
other parts of the program
• Implementation files contain definitions of the names declared in the
corresponding header
• At least the header files should include some documentation
161
Declarations and Definitions Header and Implementation Files
162
Declarations and Definitions Header and Implementation Files
path/B.hpp
#include "path/A.hpp"
inline int bar() { return foo(); }
main.cpp
#include "path/A.hpp"
#include "path/B.hpp" // ERROR: foo is defined twice
163
Declarations and Definitions Header and Implementation Files
path/B.hpp
#ifndef H_path_B
#define H_path_B
#include "path/A.hpp"
inline int bar() { return foo(); }
#endif
164
Declarations and Definitions Header and Implementation Files
The example CMake project from last lecture shows how header and
implementation files are used. These are the header files:
sayhello.hpp
#ifndef H_exampleproject_sayhello
#define H_exampleproject_sayhello
#include <string_view>
/// Print a greeting for `name`
void sayhello(std::string_view name);
#endif
saybye.hpp
#ifndef H_exampleproject_saybye
#define H_exampleproject_saybye
#include <string_view>
/// Say bye to `name`
void saybye(std::string_view name);
#endif
165
Declarations and Definitions Header and Implementation Files
saybye.cpp
#include "saybye.hpp"
#include <iostream>
166
Declarations and Definitions Header and Implementation Files
167
Compiling C++ files
168
Compiling C++ files Hello World 2.0
In C++ the code is usually separated into header files (.h/.hpp) and
implementation files (.cpp/.cc):
sayhello.hpp
#include <string_view>
void sayhello(std::string_view name);
sayhello.cpp
#include "sayhello.hpp"
#include <iostream>
void sayhello(std::string_view name) {
std::cout << "Hello " << name << '!' << std::endl;
}
Other code that wants to use this function only has to include sayhello.hpp.
169
Compiling C++ files Compiler
Compiler
Reminder: Internally, the compiler is divided into Preprocessor, Compiler, and
Linker.
Preprocessor:
• Takes an input file of (almost) any programming language
• Handles all preprocessor directives (i.e., all lines starting with #) and macros
• Outputs the file without any preprocessor directives or macros
Compiler:
• Takes a preprocessed C++ (or C) file, called translation unit
• Generates and optimizes the machine code
• Outputs an object file
Linker:
• Takes multiple object files
• Can also take references to other libraries
• Finds the address of all symbols (e.g., functions, global variables)
• Outputs an executable file or a shared library
170
Compiling C++ files Compiler
Preprocessor (1)
Preprocessor directive #include: Copies (!) the contents of a file into the
current file.
Syntax:
• #include "path" where path is a relative path to the header file
• #include <path> like the first version but only system directories are
searched for the path
In C++ usually only header files are included, never .cpp files!
171
Compiling C++ files Compiler
Preprocessor (2)
172
Compiling C++ files Compiler
Preprocessor (3)
173
Compiling C++ files Compiler
Compiler
• Every translation unit (usually a .cpp file) results in exactly one object file
(usually .o)
• References to external symbols (e.g., functions that are defined in another
.cpp) are not resolved
mul.cpp
int add(int a, int b);
int mul(int a, int b) {
if (a > 0) { return add(a, mul(a - 1, b)); }
else { return 0; }
}
Linker
• The linker usually does not have to know about any programming language
• Still, some problems with your C++ code will only be found by the linker and
not by the compiler (e.g., ODR violations)
• Most common error are missing symbols, happens either because you forgot
to define a function or global variable, or forgot to add a library
• Popular linkers are: GNU ld, GNU gold, lld (by the LLVM project)
175
Compiling C++ files Compiler
176
Compiling C++ files Debugging
• Debugging by printing text is easy but most of the time not very useful
• Especially for multi-threaded programs a real debugger is essential
• For C++ the most used debugger is gdb (“GNU debugger”)
• It is free and open-source (GPLv2)
• For the best debugging experience a program should be compiled without
optimizations (-O0) and with debug symbols (-g)
• The debug symbols help the debugger to map assembly instructions to the
source code that generated them
• The documentation for gdb can be found here:
https://sourceware.org/gdb/current/onlinedocs/gdb/
177
Compiling C++ files Debugging
178
Compiling C++ files Debugging
frame Show the currently selected stack frame, i.e. the current
stack with its local variables. Usually includes the function
name and the current source line. Can also be used to
switch to another frame.
backtrace Show all stack frames.
up Select the frame from the next higher function.
down Select the frame from the next lower function.
watch Set a watchpoint. When the memory address that is
watched is read or written, the debugger stops.
thread Show the currently selected thread in a multi-threaded pro-
gram. Can also be used to switch to another thread.
Most commands also have a short version, e.g., r for run, c for continue, etc.
179
Compiling C++ files Debugging
When this function is called with b==0, the program will crash with a useful error
message.
180
Compiling C++ files Debugging
• Modern compilers can automatically add several runtime checks, they are
usually called sanitizers
• Most important ones:
• Address Sanitizer (ASAN): Instruments memory access instructions to check
for common bugs
• Undefined-Behavior Sanitizer (UBSAN): Adds runtime checks to guard against
many kinds of undefined behavior
• Because sanitizers add overhead, they are not enabled by default
• Should normally be used in conjunction with -g for debug builds
• Compiler option for gcc/clang: -fsanitize=<sanitizer>
• -fsanitize=address for ASAN
• -fsanitize=undefined for UBSAN
• Should be enabled by default in your debug builds, unless there is a very
compelling reason against it
181
Compiling C++ files Debugging
UBSAN Example
foo.cpp
#include <iostream>
int main() {
int a; int b;
std::cin >> a >> b;
int c = a * b;
std::cout << c << std::endl;
return 0;
}
182
References, Arrays, and Pointers
183
References, Arrays, and Pointers
Overview
Much of the power of C++ comes from the ability to define compound types
• Functions
• Classes (covered in the next lecture)
• References
• Arrays
• Pointers
184
References, Arrays, and Pointers References
185
References, Arrays, and Pointers References
The & or && tokens are part of the declarator, not the type
int i = 10;
int& j = i, k = i; // j is reference to int, k is int
However, we may omit or insert whitespaces before and after the & or && tokens
• Both int& j = i; and int &j = i; are valid C++
• By convention, we use the former notation (int& j = i;)
• To avoid confusion, statements should declare only one identifier at a time
• Very rarely, exceptions to this rule are necessary in the init-statements of if
and switch statements as well as for loops
186
References, Arrays, and Pointers References
Reference Initialization
187
References, Arrays, and Pointers References
unsigned i = 10;
unsigned j = 42;
unsigned& r = i; // r is an alias for i
r = 21; // modifies i to be 21
r = j; // modifies i to be 42
i = 123;
j = r; // modifies j to be 123
188
References, Arrays, and Pointers References
int main() {
int i = 10;
foo(i); // i == 52
foo(i); // i == 94
}
189
References, Arrays, and Pointers References
int global0 = 0;
int global1 = 0;
int main() {
foo(0) = 42; // global0 == 42
foo(1) = 14; // global1 == 14
}
190
References, Arrays, and Pointers References
int i = 10;
int&& j = i; // ERROR: Cannot bind rvalue reference to lvalue
int&& k = 42; // OK
int i = 10;
int j = 32;
int&& k = i + j; // k == 42
k += 42; // k == 84;
191
References, Arrays, and Pointers References
int& bar();
int baz();
int main() {
int i = 42;
const int j = 84;
int i = 10;
const int& j = i;
int& k = j; // ERROR: binding reference of type int& to
// const int discards cv-qualifiers
j = 42; // ERROR: assignment of read-only reference
int i = 10;
int j = 32;
const int& k = i + j; // OK, but k is immutable
193
References, Arrays, and Pointers References
Dangling references
Example
int& foo() {
int i = 42;
return i; // ERROR: Returns dangling reference
}
194
References, Arrays, and Pointers Arrays
195
References, Arrays, and Pointers Arrays
a = b; // ERROR: a is an array
196
References, Arrays, and Pointers Arrays
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]
01 00 02 00 03 00 04 00 05 00 06 00 07 00 08 00 09 00 0a 00
00 02 04 06 08 0a 0c 0e 10 12
Address
197
References, Arrays, and Pointers Arrays
198
References, Arrays, and Pointers Arrays
Array Initialization
Multi-dimensional arrays may also be list-initialized, but only the first dimension
may have unknown bound
199
References, Arrays, and Pointers Arrays
size_t
C++ has a designated type for indexes and sizes: std::size_t from <cstddef>
• size_t is an unsigned integer type that is large enough to represent sizes
and all possible array indexes on the target architecture
• The C++ language and standard library use size_t when handling indexes or
sizes
• Generally, use size_t for array indexes and sizes
• Sometimes you can also use smaller integer types (e.g. unsigned) when
working with small arrays
200
References, Arrays, and Pointers Arrays
std::array
Example
#include <array>
int main() {
std::array<unsigned short, 10> a;
for (size_t i = 0; i < a.size(); ++i)
a[i] = i + 1; // no bounds checking
}
201
References, Arrays, and Pointers Arrays
std::vector (1)
Useful functions
• push_back – inserts an element at the end of the vector
• size – queries the current size
• clear – clears the contents
• resize – change the number of stored elements
• The subscript operator can be used with similar semantics as for C-style
arrays
202
References, Arrays, and Pointers Arrays
std::vector (2)
Example
#include <iostream>
#include <vector>
int main() {
std::vector<unsigned short> a;
for (size_t i = 0; i < 10; ++i)
a.push_back(i + 1);
Range-For (1)
Explanation
• Executes init-statement once, then executes loop-statement once for
each element in the range defined by range-expression
• range-expression may be an expression that represents a sequence (e.g.
an array or an object for which begin and end functions are defined, such as
std::vector)
• range-declaration should declare a named variable of the element type
of the sequence, or a reference to that type
• init-statement may be omitted
204
References, Arrays, and Pointers Arrays
Range-For (2)
Example
#include <iostream>
#include <vector>
int main() {
std::vector<unsigned short> a;
// range-for
for (const unsigned short& e : a)
std::cout << e << std::endl;
}
205
References, Arrays, and Pointers Pointers
Storage of Objects
206
References, Arrays, and Pointers Pointers
Notes
• A pointer to an object represents the address of the first byte in memory that
is occupied by that object
• As opposed to references, pointers are themselves objects
• Consequently, pointers to pointers are allowed
207
References, Arrays, and Pointers Pointers
Pointer-to-pointer declarations
Contraptions like the declaration of f are very rarely (if at all) necessary
208
References, Arrays, and Pointers Pointers
In general, there exists no implicit conversion from a pointed-to type (e.g. int) to
its pointer type (e.g. int*)
• In order to obtain a pointer to an object, the built-in unary address-of
operator & has to be used
• Given an lvalue expression a, &a returns a pointer to the value of the
expression
• The cv-qualification of a is retained
Example
int a = 10;
const int b = 42;
int* c = &a; // OK: c points to a
const int* d = &b; // OK: d points to b
int* e = &b; // ERROR: invalid conversion from
// const int* to int*
209
References, Arrays, and Pointers Pointers
Example
int a = 10;
int* c = &a;
int& d = *c; // reference to a
d = 123; // a == 123
*c = 42; // a == 42
210
References, Arrays, and Pointers Pointers
211
References, Arrays, and Pointers Pointers
0x0000122c 0a 00 00 00 a = 10
0x00001230 return address
0x00001234 unknown
00 01 02 03
212
References, Arrays, and Pointers Pointers
0x00001228 7b 00 00 00 b = 123
0x0000122c 0a 00 00 00 a = 10
0x00001230 return address
0x00001234 unknown
00 01 02 03
213
References, Arrays, and Pointers Pointers
214
References, Arrays, and Pointers Pointers
215
References, Arrays, and Pointers Pointers
216
References, Arrays, and Pointers Pointers
217
References, Arrays, and Pointers Pointers
218
References, Arrays, and Pointers Pointers
219
References, Arrays, and Pointers Pointers
220
References, Arrays, and Pointers Pointers
Null Pointers
return v;
}
221
References, Arrays, and Pointers Pointer Arithmetic
Example
#include <iostream>
int main() {
int array[3] = {123, 456, 789};
const int* ptr = array;
222
References, Arrays, and Pointers Pointer Arithmetic
Example
#include <iostream>
int main() {
int array[3] = {123, 456, 789};
const int* ptr = array;
223
References, Arrays, and Pointers Pointer Arithmetic
224
References, Arrays, and Pointers Pointer Arithmetic
#include <iostream>
int main() {
int array[3] = {123, 456, 789};
const int* ptr = &array[1];
225
References, Arrays, and Pointers Pointer Arithmetic
Example
int main() {
std::vector<int> v;
v.resize(10);
226
References, Arrays, and Pointers Pointer Arithmetic
Example
#include <iostream>
int main() {
int array[3] = {123, 456, 789};
const int* ptr1 = &array[0];
const int* ptr2 = &array[3]; // past-the-end pointer
std::cout << "There are " << (ptr2 - ptr1) << " elements ";
std::cout << "in array" << std::endl;
}
227
References, Arrays, and Pointers Pointer Arithmetic
Comparisons on Pointers
Example
#include <iostream>
int main() {
int array[3] = {123, 456, 789};
228
References, Arrays, and Pointers Pointer Conversions
Void Pointers
Usage
• Used to pass objects of unknown type
• Extensively used in C interfaces (e.g. malloc, qsort, ...)
• Only few operations are defined on void pointers (mainly assignment)
• In order to use the pointed-to object, one must cast the void pointer to the
required type
229
References, Arrays, and Pointers Pointer Conversions
static_cast (1)
Explanation
• Converts the value of expression to a value of new_type
• new_type must be at least as cv-qualified as the type of expression
• Can be used to convert void pointers to pointers of another type
• Many more use cases (see reference documentation)
230
References, Arrays, and Pointers Pointer Conversions
static_cast (2)
Void pointers
int i = 42;
void* vp = &i;
int* ip = static_cast<int*>(vp);
int main() {
int a = 42;
double b = 3.14;
231
References, Arrays, and Pointers Pointer Conversions
reinterpret_cast
Explanation
• Interprets the underlying bit pattern of the value of expression as a value
of new_type
• new_type must be at least as cv-qualified as the type of expression
• Usually does not generate any CPU instructions
232
References, Arrays, and Pointers Pointer Conversions
233
References, Arrays, and Pointers Pointer Conversions
int main() {
int a = 0;
double* y = reinterpret_cast<double*>(&a);
return foo(&a, y);
}
Compiling this with g++ -O1 will result in the following assembly
foo.o
main:
movl $0, %eax
ret
234
References, Arrays, and Pointers Pointer Conversions
int main() {
int a = 0;
double* y = reinterpret_cast<double*>(&a);
return foo(&a, y);
}
Compiling this with g++ -O2 will result in the following assembly
foo.o
main:
movl $42, %eax
ret
235
References, Arrays, and Pointers Pointer Conversions
236
References, Arrays, and Pointers Pointer Conversions
#include <iostream>
#include <iomanip>
#include <cstddef>
int main() {
double a = 3.14;
const std::byte* bytes = reinterpret_cast<const std::byte*>(&a);
237
References, Arrays, and Pointers Pointer Conversions
uintptr_t
Example
#include <cstddint>
#include <iostream>
int main() {
int x = 42;
uintptr_t addr = reinterpret_cast<uintptr_t>(&x);
238
References, Arrays, and Pointers Pointer Conversions
The sizeof operator queries the size of the object representation of a type
sizeof( type )
Explanation
• The size of a type is given in bytes
• sizeof(std::byte), sizeof(char), and sizeof(unsigned char)
return 1 by definition
• Depending on the computer architecture, there may be 8 or more bits in one
byte (as defined by C++)
239
References, Arrays, and Pointers Pointer Conversions
int main() {
int array[3] = {123, 456, 789};
std::cout << "(ptr1 - ptr0) = " << (ptr1 - ptr0) << std::endl;
std::cout << "(uptr1 - uptr0) = " << (uptr1 - uptr0) << std::endl;
}
240
References, Arrays, and Pointers Pointer Conversions
$ ./foo
sizeof(int) = 4
(ptr1 - ptr0) = 1
(uptr1 - uptr0) = 4
Interpretation
• One int occupies 4 bytes
• There is one int between ptr0 and ptr1
• There are 4 bytes (i.e. exactly one int) between ptr0 and ptr1
241
References, Arrays, and Pointers Pointer Conversions
alignof( type )
Explanation
• Depending on the computer architecture, certain types must have addresses
aligned to specific byte boundaries
• The alignof operator returns the number of bytes between successive
addresses where an object of type can be allocated
• The alignment requirement of a type is always a power of two
• Important (e.g.) for SIMD instructions, where the programmer must
explicitly ensure correct alignment
• Memory accesses with incorrect alignment leads to undefined behavior, e.g.
SIGSEGV or SIGBUS (depending on architecture)
242
References, Arrays, and Pointers Guidelines
Usage Guidelines
When to use references
• Pass-by-reference function call semantics
• When it is guaranteed that the referenced object will always be valid
• When object that should be referenced is always the same
243
References, Arrays, and Pointers Troubleshooting
Troubleshooting
244
References, Arrays, and Pointers Troubleshooting
Obvious example
foo.cpp
int main() {
int* a;
return *a; // ERROR: Dereferencing an uninitialized pointer
}
$ ./foo
[1] 5128 segmentation fault (core dumped) ./foo
245
References, Arrays, and Pointers Troubleshooting
return *ptr;
}
246
References, Arrays, and Pointers Troubleshooting
247
Classes
Classes
248
Classes
Classes
249
Classes Members
Data Members
• Declarations of data members are variable declarations
• extern is not allowed
• Declarations without static are called non-static data members, otherwise
they are static data members
• thread_local is only allowed for static data members
• Declaration must have a complete type (see later slide)
• Name of the declaration must differ from the class name and must be unique
within the class
• Non-static data members can have a default value
struct Foo {
// non-static data members:
int a = 123;
float& b;
const char c;
// static data members:
static int s;
thread_local static int t;
};
250
Classes Members
251
Classes Members
sizeof(i) == 4
alignof(i) == 4 offset
struct C { 00 01 02 03 04 05 06 07
int i; sizeof(p) == 8
alignof(p) == 8
00 i padding
int* p;
08 p
char b; sizeof(b) == 1
short s; 10 b s padding
alignof(b) == 1
};
sizeof(s) == 2
alignof(s) == 2
sizeof(C) == 24
alignof(C) == 8
Reordering the member variables in the order p, i, s, b would lead to
sizeof(C) == 16!
In general: Order member variables by decreasing alignment to get the fewest
padding bytes.
252
Classes Members
Member Functions
• Declarations of member functions are like regular function declarations
• Just like for data members, there are non-static and static (with the static
specifier) member functions
• Non-static member functions can be const-qualified (with const) or
ref-qualified (with const&, &, or &&)
• Non-static member functions can be virtual
• There are some member functions with special functions:
• Constructor and destructor
• Overloaded operators
struct Foo {
void foo(); // non-static member function
void cfoo() const; // const-qualified non-static member function
void rfoo() &; // ref-qualified non-static member function
static void bar(); // static member function
Foo(); // Constructor
~Foo(); // Destructor
bool operator==(const Foo& f); // Overloaded operator ==
};
253
Classes Members
Accessing Members
254
Classes Members
struct C {
int i;
int foo() {
this->i; // Explicit member access, this has type C*
return i; // Implicit member access
}
int foo() const { return this->i; /* this has type const C* */ }
int bar() & { return i; /* this (implicit) has type C* */ }
int bar() const& { return this->i; /* this has type const C* */ }
};
255
Classes Members
Out-of-line Definitions
• Just like regular functions member functions can have separate declarations
and definitions
• A member function that is defined in the class body is said to have an inline
definition
• A member function that is defined outside of the class body is said to have
an out-of-line definition
• Member functions with inline definitions implicitly have the inline specifier
• Out-of-line definitions must have the same qualifiers as their declaration
struct Foo {
void foo1() { /* ... */ } // Inline definition
void foo2();
void foo_const() const;
static void foo_static();
};
// Out-of-line definitions
void Foo::foo2() { /* ... */ }
void Foo::foo_const() const { /* ... */ }
void Foo::foo_static() { /* ... */ }
256
Classes Forward Declarations
Use Cases
• Allows classes to refer to each other
• Can reduce compilation time (significantly) by avoiding transitive includes of
an expensive-to-compile header
• Commonly used in header files
257
Classes Forward Declarations
Example
foo.hpp
class A;
class ClassFromExpensiveHeader;
class B {
ClassFromExpensiveHeader* member;
foo.cpp
#include "expensive_header.hpp"
/* implementation */
258
Classes Forward Declarations
Incomplete Types
259
Classes Constructors and Destructors
Constructors
• Constructors are special functions that are called when an object is initialized
• Constructors have no return type, no const- or ref-qualifiers, and their name
is equal to the class name
• The definition of a constructor can have an initializer list
• Constructors can have arguments, a constructor without arguments is called
default constructor
• Constructors are sometimes implicitly defined by the compiler
260
Classes Constructors and Destructors
Initializer List
• The initializer list specifies how member variables are initialized before the
body of the constructor is executed
• Other constructors can be called in the initializer list
• Members should be initialized in the order of their definition
• Members are initialized to their default value if not specified in the list
• const member variables can only be initialized in the initializer list
struct Foo {
int a = 123; float b; const char c;
// default constructor initializes a (to 123), b, and c
Foo() : b(2.5), c(7) {}
// initializes a and b to the given values
Foo(int a, float b, char c) : a(a), b(b), c(c) {}
Foo(float f) : Foo() {
// First the default constructor is called, then the body
// of this constructor is executed
b *= f;
}
};
261
Classes Constructors and Destructors
Initializing Objects
262
Classes Constructors and Destructors
• Constructors with exactly one argument are treated specially: They are used
for explicit and implicit conversions
• If implicit conversion with such constructors is not desired, the keyword
explicit can be used to disallow it
• Generally, you should use explicit unless you have a good reason not to
263
Classes Constructors and Destructors
Copy Constructors
struct Foo {
Foo(const Foo& other) { /* ... */ }
};
void doFoo(Foo f);
Foo f;
Foo g(f); // Call copy constructor explicitly
doFoo(g); // Copy constructor is called implicitly
264
Classes Constructors and Destructors
Destructors
Foo a;
Bar b;
{
Baz c;
// c.~Baz() is called;
}
// b.~Bar() is called
// a.~Foo() is called
265
Classes Constructors and Destructors
Writing Destructors
struct Foo {
Bar a;
Bar b;
~Foo() {
std::cout << "Bye\n";
// b.~Bar() is called
// a.~Bar() is called
}
};
266
Classes Member Access Control
class Foo {
int a; // a is private
public:
// All following declarations are public
int b;
int getA() const { return a; }
protected:
// All following declarations are protected
int c;
public:
// All following declarations are public
static int getX() { return 123; }
};
267
Classes Member Access Control
Notes
• Friendship is non-transitive and cannot be inherited
• Access specifiers have no influence on friend declarations (i.e. they can
appear in private: or public: sections)
268
Classes Member Access Control
class A {
int a;
friend class B;
friend void foo(A&);
};
class B {
friend class C;
void bar(A& a) {
a.a = 42; // OK
}
};
class C {
void foo(A& a) {
a.a = 42; // ERROR
}
};
void foo(A& a) {
a.a = 42; // OK
}
269
Classes Member Access Control
Nested Types
struct A {
struct B {
int getI(const A& a) {
return a.i; // OK, B is friend of A
}
};
private:
int i;
};
A::B b; // reference nested type B of class A
270
Classes Constness of Members
struct Foo {
Expression Value Category
int i; foo.i non-const lvalue
const int c; foo.c const lvalue
mutable int m; foo.m non-const lvalue
} cfoo.i const lvalue
Foo& foo = /* ... */; cfoo.c const lvalue
const Foo& cfoo = /* ... */; cfoo.m non-const lvalue
271
Classes Constness of Members
272
Classes Constness of Members
struct Foo {
int a;
};
const Foo f{123};
Foo& fref = const_cast<Foo&>(f); // OK, cast is allowed
int b = fref.a; // OK, accessing value is allowed
fref.a = 42; // undefined behavior
273
Classes Constness of Members
class A {
int* numbers;
int& foo() {
int i = /* ... */;
// do some incredibly complicated computation to
// get a value for i
return numbers[i]
}
const int& foo() const {
// OK as long as foo() does not modify the object
return const_cast<A&>(*this).foo();
}
};
274
Classes Operator Overloading
Operator Overloading
• Classes can have special member functions to overload built-in operators like
+, ==, etc.
• Many overloaded operators can also be written as non-member functions
• Syntax: return-type operator op (arguments)
• Overloaded operator functions are selected with the regular overload
resolution
• Overloaded operators are not required to have meaningful semantics
• Almost all operators can be overloaded, exceptions are: :: (scope
resolution), . (member access), .* (member pointer access), ?: (ternary
operator)
• This includes “unusual” operators like: = (assignment), () (call),
* (dereference), & (address-of), , (comma)
275
Classes Operator Overloading
Arithmetic Operators
The expression lhs op rhs is mostly equivalent to lhs.operator op(rhs) or
operator op(lhs, rhs) for binary operators.
• As calls to overloaded operators are treated like regular function calls, the
overloaded versions of || and && lose their special behaviors
• Should be const and take const references
• Usually return a value and not a reference
• The unary + and − operators can be overloaded as well
struct Int {
int i;
Int operator+(const Int& other) const { return Int{i + other.i}; }
Int operator-() const { return Int{-i}; };
};
Int operator*(const Int& a, const Int& b) { return Int{a.i * b.i}; }
a + b; /* is equivalent to */ a.operator+(b);
a * b; /* is equivalent to */ operator*(a, b);
-a; /* is equivalent to */ a.operator-();
276
Classes Operator Overloading
Comparison Operators
All binary comparison operators (<, <=, >, >=, ==, !=, <=>) can be overloaded.
• Should be const and take const references
• Return bool, except for <=> (see next slide)
• If only operator<=> is implemented, <, <=, >, and >= work as well
• operator== must be implemented separately
• If operator== is implemented, != works as well
struct Int {
int i;
std::strong_ordering operator<=>(const Int& a) const {
return i <=> a.i;
}
bool operator==(const Int& a) const { return i == a.i; }
};
Int a{123}; Int b{456};
a < b; /* is equivalent to */ (a.operator<=>(b)) < 0;
a == b; /* is equivalent to */ a.operator==(b);
277
Classes Operator Overloading
The overloaded operator<=> should return one of the following three types from
<compare>: std::partial_ordering, std::weak_ordering,
std::strong_ordering.
• When comparing two values a and b with ord = (a <=> b), then ord has
one of the three types and can be compared to 0:
• ord == 0 ⇔ a == b
• ord < 0 ⇔ a < b
• ord > 0 ⇔ a > b
• std::strong_ordering can be converted to std::weak_ordering and
std::partial_ordering
• std::weak_ordering can be converted to std::partial_ordering
278
Classes Operator Overloading
279
Classes Operator Overloading
280
Classes Operator Overloading
struct Int {
int i;
Int& operator++() { ++i; return *this; }
Int operator--(int) { Int copy{*this}; --i; return copy; }
};
Int a{123};
++a; // a.i is now 124
a++; // ERROR: post-increment is not overloaded
Int b = a--; // b.i is 124, a.i is 123
--b; // ERROR: pre-decrement is not overloaded
281
Classes Operator Overloading
Subscript Operator
Classes that behave like containers or pointers usually override the subscript
operator [].
• a[b] is equivalent to a.operator[](b)
• Type of b can be anything, for array-like containers it is usually size_t
282
Classes Operator Overloading
Dereference Operators
Classes that behave like pointers usually override the operators * (dereference)
and -> (member of pointer).
• operator*() usually returns a reference
• operator->() should return a pointer or an object that itself has an
overloaded -> operator
283
Classes Operator Overloading
Assignment Operators
• The simple assignment operator is often used together with the copy
constructor and should have the same semantics
• All assignment operators usually return *this
struct Int {
int i;
Foo& operator=(const Foo& other) { i = other.i; return *this; }
Foo& operator+=(const Foo& other) { i += other.i; return *this; }
};
Foo a{123};
a = Foo{456}; // a.i is now 456
a += Foo{1}; // a.i is now 457
284
Classes Operator Overloading
Conversion Operators
A class C can use converting constructors to convert values of other types to type
C. Similarly, conversion operators can be used to convert objects of type C to
other types.
Syntax: operator type ()
• Conversion operators have the implicit return type type
• They are usually declared as const
• The explicit keyword can be used to prevent implicit conversions
• Explicit conversions are done with static_cast
• operator bool() is usually overloaded to be able to use objects in an if
statement
285
Classes Operator Overloading
Argument-Dependent Lookup
• Overloaded operators are usually defined in the same namespace as the type
of one of their arguments
• Regular unqualified lookup would not allow the following example to compile
• To fix this, unqualified names of functions are also looked up in the
namespaces of all arguments
• This is called Argument Dependent Lookup (ADL)
286
Classes Defaulted and Deleted Member Functions
struct Int128 {
int64_t x; int64_t y;
std::strong_ordering operator<=>(const Int&) const = default;
};
Int128 a{0, 123}; Int128 b{1, 0};
a < b; // true
a == b; // false
a <=> b; // std::strong_ordering::less
288
Classes Defaulted and Deleted Member Functions
struct Foo {
Foo(const Foo&) = delete;
};
Foo f; // Default constructor is defined implicitly
Foo g(f); // ERROR: copy constructor is deleted
289
Other User-Defined Types
290
Other User-Defined Types Unions
Unions
• In addition to regular classes declared with class or struct, there is
another special class type declared with union
• In a union only one member may be “active”, all members use the same
storage
• Size of the union is equal to size of largest member
• Alignment of the union is equal to largest alignment among members
• Strict aliasing rule still applies with unions!
• Most of the time there are better alternatives to unions, e.g.
std::array<std::byte, N> or std::variant
Enums
• C++ also has user-defined enumeration types
• Typically used like integral types with a restricted range of values
• Also used to be able to use descriptive names instead of “magic” integer
values
• Syntax: enum-key name { enum-list };
• enum-key can be enum, enum class, or enum struct
• enum-list consists of comma-separated entries with the following syntax:
name [ = value ]
• When value is not specified, it is automatically chosen starting from 0
enum Color {
Red, // Red == 0
Blue, // Blue == 1
Green, // Green == 2
White = 10,
Black, // Black == 11
Transparent = White // Transparent == 10
};
292
Other User-Defined Types Enums
• Names from the enum list can be accessed with the scope resolution operator
• When enum is used as keyword, names are also introduced in the enclosing
namespace
• Enums declared with enum can be converted implicitly to int
• Enums can be converted to integers and vice versa with static_cast
• enum class and enum struct are equivalent
• Guideline: Use enum class unless you have a good reason not to
293
Other User-Defined Types Type Aliases
Type Aliases
• Names of types that are nested deeply in multiple namespaces or classes can
become very long
• Sometimes it is useful to declare a nested type that refers to another, existing
type
• For this type aliases can be used
• Syntax: using name = type;
• name is the name of the alias, type must be an existing type
• For compatibility with C type aliases can also be defined with typedef with
a different syntax but this should never be used in modern C++ code
294
Other User-Defined Types Type Aliases
In C++ the following aliases are defined in the std namespace and are commonly
used:
intN_t: Integer types with exactly N bits, usually defined for 8, 16, 32, and
64 bits
uintN_t: Similar to intN_t but unsigned
size_t: Used by the standard library containers everywhere a size or index
is needed, also result type of sizeof and alignof
uintptr_t: An integer type that is guaranteed to be able to hold all possible
values that result from a reinterpret_cast from any pointer
intptr_t: Similar to uintptr_t but signed
ptrdiff_t: Result type of expressions that subtract two pointers
max_align_t: Type which has alignment as least as large as all other scalar
types
295
Iterators
Iterators
296
Iterators
Be careful: When writing to a container, all existing iterators are invalidated and
can no longer be used (some exceptions apply)!
297
Iterators
The end iterator points to the first element after the container. Dereferencing it
results in undefined behavior:
*end; // undefined behavior
An iterator can be incremented (just like a pointer) to point at the next element:
++it; // Prefer to use pre-increment
std::cout << *it; // prints "two"
298
Iterators
Iterators can be checked for equality. Comparing to the end iterator is used to
check whether iteration is done:
// prints "three,four,"
for (; it != end; ++it) {
std::cout << *it << ",";
}
Such a loop requires the range expression (here: vec) to have a begin() and
end() member.
vec.begin() is assigned to an internal iterator which is dereferenced, assigned
to the range declaration (here: auto elem), and then incremented until it equals
vec.end().
299
Iterators
input_iterator, output_iterator
The standard library defines several concepts for different kinds of iterators in the
<iterator> header. std::input/output_iterator are the most basic
iterators. They have the following features:
• Equality comparison: Checks if two iterators point to the same position
• Dereferencable with the * and -> operators
• Incrementable, to point at the next element in sequence
• A dereferenced std::input_iterator can only by read
• A dereferenced std::output_iterator can only be written to
As the most restrictive iterators, they have a few limitations:
• Single-pass only: They cannot be decremented
• Only allow equality comparison, <, >=, etc. not supported
• Can only be incremented by one (i.e. it + 2 does not work)
Used in single-pass algorithms such as find() (std::input_iterator) or
copy() (Copying from an std::input_iterator to an
std::output_iterator)
301
Iterators
forward_iterator, bidirectional_iterator
302
Iterators
random_access_iterator, contiguous_iterator
std::random_access_iterator generalizes
std::bidirectional_iterator
• Additionally allows random access with operator[]
• Supports relational operators, such as < or >=
• Can be incremented or decremented by any amount (i.e. it + 2 does work)
303
Dynamic Memory Management
304
Dynamic Memory Management Process Memory Layout
Each Linux process runs within its own virtual address space
• The kernel pretends that each process has access to a (huge) continuous
range of addresses (≈ 256 TiB on x86-64)
• Virtual addresses are mapped to physical addresses by the kernel using page
tables and the MMU (if available)
• Greatly simplifies memory management code in the kernel and improves
security due to memory isolation
• Allows for useful “tricks” such as memory-mapping files
305
Dynamic Memory Management Process Memory Layout
306
Dynamic Memory Management Process Memory Layout
307
Dynamic Memory Management Process Memory Layout
Stack memory is typically used for objects with automatic storage duration
• The compiler can statically decide when allocations and deallocations must
happen
• The memory layout is known at compile-time
• Allows for highly optimized code (allocations and deallocations simply
increase/decrease a pointer)
308
Dynamic Memory Management Process Memory Layout
Example
foo.cpp foo.o
int foo() { foo():
int c = 2; pushq %rbp
int d = 21; movq %rsp, %rbp
movl $2, -4(%rbp)
return c * d; movl $21, -8(%rbp)
} movl -4(%rbp), %eax
imull -8(%rbp), %eax
int main() { popq %rbp
int a[100]; ret
int b = foo(); main:
pushq %rbp
return b; movq %rsp, %rbp
} subq $416, %rsp
call foo()
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
leave
ret
309
Dynamic Memory Management Process Memory Layout
Heap Segment
The heap is typically used for objects with dynamic storage duration
• The programmer must explicitly manage allocations and deallocations
• Allows much more flexible programs
Disadvantages
• Performance impact due to non-trivial implementation of heap-based memory
allocation
• Memory fragmentation
• Dynamic memory allocation is error-prone
• Memory leaks
• Double free (deallocation)
• Make use of debugging tools (GDB, ASAN (!))
310
Dynamic Memory Management Dynamic Memory Management in C++
Mechanisms give control over the storage duration and possibly lifetime of objects
• Level of control varies by method
• In all cases: Manual intervention required
311
Dynamic Memory Management Dynamic Memory Management in C++
Explanation
• Allocates heap storage for a single object or an array of objects
• Constructs and initializes a single object or an array of objects in the newly
allocated storage
• If initializer is absent, the object is default-initialized
• Returns a pointer to the object or the initial element of the array
312
Dynamic Memory Management Dynamic Memory Management in C++
Explanation
• If expression is nullptr nothing is done
• Invokes the destructor of the object that is being destroyed, or of every object
in the array that is being destroyed
• Deallocates the memory previously occupied by the object(s)
313
Dynamic Memory Management Dynamic Memory Management in C++
Node* first;
Node* last;
public:
~IntList() {
while (first != nullptr) {
Node* next = first->next;
delete first;
first = next;
}
}
void push_back(int i) {
Node* node = new Node{i, nullptr};
if (!last)
first = node;
else
last->next = node;
last = node;
}
};
314
Dynamic Memory Management Dynamic Memory Management in C++
Memory Leaks
if (condition)
return 42; // MEMORY LEAK
delete[] buffer;
return 123;
}
Avoid explicit memory management through new and delete whenever possible
315
Dynamic Memory Management Dynamic Memory Management in C++
316
Dynamic Memory Management Dynamic Memory Management in C++
Example
#include <cstddef>
#include <new>
struct A { };
int main() {
std::byte* buffer = new std::byte[sizeof(A)];
A* a = new (buffer) A();
/* ... do something with a ... */
a->~A(); // we must explicitly call the destructor
delete[] buffer;
}
317
Dynamic Memory Management Dynamic Memory Management in C++
The lifetime of an object is equal to or nested within the lifetime of its storage
• Equal for regular new and delete
• Possibly nested for placement new
Example
struct A { };
int main() {
A* a1 = new A(); // lifetime of a1 begins, storage begins
a1->~A(); // lifetime of a1 ends
A* a2 = new (a1) A(); // lifetime of a2 begins
delete a2; // lifetime of a2 ends, storage ends
}
318
Dynamic Memory Management Dynamic Memory Management in C++
319
Dynamic Memory Management Memory Manipulation Primitives
std::memcpy (1)
320
Dynamic Memory Management Memory Manipulation Primitives
std::memcpy (2)
Example (straightforward copy)
#include <cstring>
#include <vector>
int main() {
std::vector<int> buffer = {1, 2, 3, 4};
buffer.resize(8);
std::memcpy(&buffer[4], &buffer[0], 4 * sizeof(int));
}
#include <cstring>
#include <cstdint>
int main() {
int64_t i = 42;
double j;
std::memcpy(&j, &i, sizeof(double)); // OK
}
321
Dynamic Memory Management Memory Manipulation Primitives
std::memmove (1)
322
Dynamic Memory Management Memory Manipulation Primitives
std::memmove (2)
#include <cstring>
#include <vector>
int main() {
std::vector<int> buffer = {1, 2, 3, 4};
buffer.resize(6);
std::memmove(&buffer[2], &buffer[0], 4 * sizeof(int));
// buffer is now {1, 2, 1, 2, 3, 4}
}
323
Copy and Move Semantics
324
Copy and Move Semantics Copy Semantics
Copy Semantics
325
Copy and Move Semantics Copy Semantics
326
Copy and Move Semantics Copy Semantics
class A {
private:
int v;
public:
explicit A(int v) : v(v) { }
A(const A& other) : v(other.v) { }
};
int main() {
A a1(42); // calls A(int)
327
Copy and Move Semantics Copy Semantics
Explanation
• Called whenever selected by overload resolution
• Returns a reference to the object itself (i.e. *this) to allow for chaining
assignments
328
Copy and Move Semantics Copy Semantics
class A {
private:
int v;
public:
explicit A(int v) : v(v) { }
A(const A& other) : v(other.v) { }
int main() {
A a1(42); // calls A(int)
A a2 = a1; // calls copy constructor
329
Copy and Move Semantics Copy Semantics
330
Copy and Move Semantics Copy Semantics
Implicit Definition
332
Copy and Move Semantics Copy Semantics
Example
struct A {
const int v;
int main() {
A a1(42);
333
Copy and Move Semantics Copy Semantics
334
Copy and Move Semantics Copy Semantics
#include <vector>
struct A {
int b;
double c;
};
int main() {
std::vector<A> buffer1;
buffer1.resize(10);
335
Copy and Move Semantics Copy Semantics
336
Copy and Move Semantics Copy Semantics
return *this;
}
};
337
Copy and Move Semantics Move Semantics
Move Semantics
338
Copy and Move Semantics Move Semantics
Typically called when an object is initialized from an rvalue of the same type
• Syntax: class_name ( class_name&& ) noexcept
• class_name must be the name of the current class
• The noexcept keyword should be added to indicate that the constructor
never throws an exception
Explanation
• Overload resolution decides if the copy or move constructor of an object
should be called
• Temporary values and calls to functions that return an object are rvalues
• The std::move function in the <utility> header may be used to convert
an lvalue to an rvalue
• We know that the argument does not need its resources anymore, so we can
simply steal them
339
Copy and Move Semantics Move Semantics
struct A {
A(const A& other);
A(A&& other);
};
A getA();
int main() {
A a1;
A a2(a1); // calls copy constructor
A a3(std::move(a1)); // calls move constructor
A a4(getA()); // calls move constructor
}
340
Copy and Move Semantics Move Semantics
Explanation
• Overload resolution decides if the copy or move assignment operator of an
object should be called
• We know that the argument does not need its resources anymore, so we can
simply steal them
• The move assignment operator returns a reference to the object itself (i.e.
*this) to allow for chaining
341
Copy and Move Semantics Move Semantics
struct A {
A();
A(const A&);
A(A&&) noexcept;
int main() {
A a1;
A a2 = a1; // calls copy-constructor
A a3 = std::move(a1); // calls move-constructor
342
Copy and Move Semantics Move Semantics
The compiler will implicitly declare a public move constructor if all the following
conditions hold
• There are no user-declared copy constructors
• There are no user-declared copy assignment operators
• There are no user-declared move assignment operators
• There are no user-declared destructors
343
Copy and Move Semantics Move Semantics
Implicit Definition
345
Copy and Move Semantics Move Semantics
Example
struct A {
const int v;
int main() {
A a1(42);
346
Copy and Move Semantics Move Semantics
347
Copy and Move Semantics Move Semantics
348
Copy and Move Semantics Move Semantics
delete[] memory;
capacity = other.capacity;
memory = other.memory;
other.capacity = 0;
other.memory = nullptr;
return *this;
}
};
349
Copy and Move Semantics Move Semantics
Compilers must omit copy and move constructors under certain circumstances
• Objects are instead directly constructed in the storage into which they would
be copied/moved
• Results in zero-copy pass-by-value semantics
• Most importantly in return statements and variable initialization from a
temporary
• More optimizations allowed, but not required
350
Copy and Move Semantics Move Semantics
struct A {
int a;
A(int a) : a(a) {
std::cout << "constructed" << std::endl;
}
A foo() {
return A(42);
}
int main() {
A a = foo(); // prints only "constructed"
}
351
Copy and Move Semantics Move Semantics
Value Categories
cannot
move
• xvalues identify an object whose lvalue
resources can be reused
• prvalues compute the value of an
rvalue
move
xvalue prvalue
can
operand or initialize an object
glvalue
In particular, std::move just converts its argument to an xvalue expression
• std::move is exactly equivalent to a static_cast to an rvalue reference
• std::move is exclusively syntactic sugar (to guide overload resolution)
352
Copy and Move Semantics Idioms
Copy-And-Swap (1)
Implementation
• Exchange the resources between the argument and *this;
• Let the destructor clean up the resources of the argument
353
Copy and Move Semantics Idioms
Copy-And-Swap (2)
Example
#include <algorithm>
#include <cstring>
struct A {
unsigned capacity;
int* memory;
return *this;
} // destructor cleans up resources formerly held by *this
};
If a class requires one of the following, it almost certainly requires all three
• A user-defined destructor
• A user-defined copy constructor
• A user-defined copy assignment operator
Explanation
• Having a user-defined copy constructor usually implies some custom setup
logic which needs to be executed by copy assignment and vice-versa
• Having a user-defined destructor usually implies some custom cleanup logic
which needs to be executed by copy assignment and vice-versa
• The implicitly-defined versions are usually incorrect if a class manages a
resource of non-class type (e.g. a raw pointer, POSIX file descriptor, etc.)
355
Copy and Move Semantics Idioms
If a class follows the rule of three, move operations are defined as deleted
• If move semantics are desired for a class, it has to define all five special
member functions
• If only move semantics are desired for a class, it still has to define all five
special member functions, but define the copy operations as deleted
Explanation
• Not adhering to the rule of five usually does not lead to incorrect code
• However, many optimization opportunities may be inaccessible to the
compiler if no move operations are defined
356
Copy and Move Semantics Idioms
Bind the lifetime of a resource that has to be allocated to the lifetime of an object
• Resources can be allocated heap memory, sockets, files, mutexes, disk space,
database connections, etc.
• Guarantees availability of the resource during the lifetime of the object
• Guarantees that resources are released when the lifetime of the object ends
• Object should have automatic storage duration
• Known as the Resource Acquisition is Initialization (RAII) idiom
357
Copy and Move Semantics Idioms
Implementation of RAII
• Encapsulate each resource into a class whose sole responsibility is managing
the resource
• The constructor acquires the resource and establishes all class invariants
• The destructor releases the resource
• Typically, copy operations should be deleted and custom move operations
need to be implemented
358
Copy and Move Semantics Idioms
359
Copy and Move Semantics Idioms
if (condition)
return false; // no worries about forgetting to free memory
/* do something more */
int main() {
CustomIntBuffer buffer(5);
return foo(std::move(buffer));
}
360
Ownership
Ownership
361
Ownership
Ownership Semantics
The RAII idiom and move semantics together enable ownership semantics
• A resource should be “owned”, i.e. encapsulated, by exactly one C++ object
at all times
• Ownership can only be transferred explicitly by moving the respective object
• E.g., the CustomIntBuffer class implements ownership semantics for a
dynamically allocated int-array
362
Ownership Smart Pointers
std::unique_ptr (1)
363
Ownership Smart Pointers
std::unique_ptr (2)
Usage of std::unique_ptr (for details: see reference documentation)
Creation
• std::make_unique<type>(arg0, ..., argN), where arg0, ...,
argN are passed to the constructor of type
Conversion to bool
• std::unique_ptr is contextually convertible to bool, i.e. it can be used in
if statements in the same way as raw pointers
std::unique_ptr (3)
Example
#include <memory>
struct A {
int a;
int b;
365
Ownership Smart Pointers
std::unique_ptr (4)
int main() {
std::unique_ptr<int[]> buffer = foo(42);
/* do something */
}
366
Ownership Smart Pointers
std::shared_ptr (1)
Usage of std::shared_ptr
• Use std::make_shared for creation
• Remaining operations analogous to std::unique_ptr
• For details: See the reference documentation
367
Ownership Smart Pointers
std::shared_ptr (2)
Example
#include <memory>
#include <vector>
struct Node {
std::vector<std::shared_ptr<Node>> children;
int main() {
Node root;
root.addChild(std::make_shared<Node>());
root.addChild(std::make_shared<Node>());
root.children[0]->addChild(root.children[1]);
368
Ownership Smart Pointers
369
Ownership Smart Pointers
370
Ownership Smart Pointers
struct A { };
int main() {
A a;
readA(a);
readWriteA(a);
workOnCopyOfA(a);
consumeA(std::move(a)); // cannot call without std::move
}
371
Ownership Smart Pointers
When dealing with an object of type T use the following rough guidelines to
decide which type to use when passing it as function argument:
Situation Type to Use
•Ownership of object should be transferred to T
callee
•Potential copies are acceptable or T is not copy-
able
•Object is relatively small (at most ≈ one cache
line)
372
Ownership Smart Pointers
373
Inheritance
Inheritance
374
Inheritance
Object-Oriented Programming
• Inheritance
• Implemented by class derivation in C++
• Derived Classes inherit the members of its base class(es)
• Covered in this lecture
375
Inheritance Basic Non-Polymorphic Inheritance
Any class type may be derived from one or more base classes
• Possible for both class and struct
• Base classes may in turn be derived from their own base classes
• Classes form an inheritance hierarchy
High-level Syntax
376
Inheritance Basic Non-Polymorphic Inheritance
Explanation
• access-specifier controls the inheritance mode (more details soon)
• access-specifier is optional; if present it can be one of the keywords
private, protected or public
• base-class-name is mandatory, it specifies the name of the class from
which to derive
• virtual-specifier is optional; if present it must be the keyword
virtual (only used for multiple inheritance)
377
Inheritance Basic Non-Polymorphic Inheritance
Examples
class Base {
int a;
};
378
Inheritance Basic Non-Polymorphic Inheritance
The initialization order is independent of any order in the member initializer list
379
Inheritance Basic Non-Polymorphic Inheritance
Derived(); Base::Base(int a)
Derived(int a, int b); : a(a) {
}; cout << "Base::Base(int)" << endl;
}
Derived::Derived() {
: b(42) {
cout << "Derived::Derived()" << endl;
}
Derived::Derived(int a, int b)
: Base(a), b(b) {
cout << "Derived::Derived(int, int)" << endl;
}
380
Inheritance Basic Non-Polymorphic Inheritance
int main() {
Derived derived0;
Derived derived1(123, 456);
}
$ ./foo
Base::Base()
Derived::Derived()
Base::Base(int)
Derived::Derived(int, int)
381
Inheritance Basic Non-Polymorphic Inheritance
Destructors (1)
The order in which the base class destructors are called is deterministic
• It depends on the order of construction, which in turn only depends on the
order of base classes in the base-specifier-list
382
Inheritance Basic Non-Polymorphic Inheritance
Destructors (2)
Derived::~Derived() {
cout << "Derived::~Derived()" << endl;
}
383
Inheritance Basic Non-Polymorphic Inheritance
Destructors (3)
int main() {
Derived derived;
}
$ ./foo
Derived::~Derived()
Base1::~Base1()
Base0::~Base0()
384
Inheritance Basic Non-Polymorphic Inheritance
Multiple inheritance can lead to additional problems even without reusing a name
• In a diamond-shaped inheritance hierarchy, members of the root class appear
twice in the most derived class
• Can be solved with virtual inheritance
• Should still be avoided whenever possible
385
Inheritance Basic Non-Polymorphic Inheritance
struct A {
void a();
};
struct B : A {
void a();
void b() {
a(); // calls B::a()
}
};
struct C : B {
void c() {
a(); // calls B::a()
}
};
386
Inheritance Basic Non-Polymorphic Inheritance
struct X {
void x();
};
struct B1 : X { };
struct B2 : X { };
struct D : B1, B2 {
void d() {
x(); // ERROR: x is present in B1 and B2
}
};
387
Inheritance Basic Non-Polymorphic Inheritance
struct A {
void a();
};
struct B : A {
void a();
};
int main() {
B b;
b.a(); // calls B::a()
b.A::a(); // calls A::a()
}
388
Inheritance Basic Non-Polymorphic Inheritance
Object Representation
The object representation of derived class objects accounts for inheritance
• The base class object is stored as a subobject in the derived class object
• Thus, derived classes may still be trivially constructible, copyable, or
destructible
foo.cpp foo.o
struct A { main:
int a = 42; pushq %rbp
int b = 123; movq %rsp, %rbp
}; movl $42, -12(%rbp)
movl $123, -8(%rbp)
struct B : A { movl $456, -4(%rbp)
int c = 456; movl $0, %eax
}; popq %rbp
ret
int main() {
B b;
}
389
Inheritance Polymorphic Inheritance
Polymorphic Inheritance
By default, inheritance in C++ is non-polymorphic
• Member definitions in a derived class can hide definitions in the base class
• For example, it matters if we call a function through a pointer to a base
object or a pointer to a derived object
#include <iostream>
struct Base {
void foo() { std::cout << "Base::foo()" << std::endl; }
};
int main() {
Derived d;
Base& b = d;
390
Inheritance Polymorphic Inheritance
391
Inheritance Polymorphic Inheritance
struct Base {
virtual void foo() { std::cout << "Base::foo()" << std::endl; }
};
struct Derived : Base {
void foo() { std::cout << "Derived::foo()" << std::endl; }
};
int main() {
Base b;
Derived d;
Base& br = b;
Base& dr = d;
If these conditions are met, the function overrides the virtual base class function
• The derived function is also virtual and can be overridden by further-derived
classes
• The base class function does not need to be visible
• The return type must be the same or covariant
If these conditions are not met, the function may hide the virtual base class
function
393
Inheritance Polymorphic Inheritance
struct Base {
private:
virtual void bar();
public:
virtual void foo();
};
int main() {
Derived d;
Base& b = d;
394
Inheritance Polymorphic Inheritance
A derived class can also inherit a function that overrides a virtual base class
function through multiple inheritance
• There must only be one final overrider at all times
• Multiple inheritance should be avoided anyway
395
Inheritance Polymorphic Inheritance
struct A {
virtual void foo();
virtual void bar();
virtual void baz();
};
struct B : A {
void foo();
void bar();
};
struct C : B {
void foo();
};
int main() {
C c;
A& cr = c;
396
Inheritance Polymorphic Inheritance
struct A {
virtual void foo();
virtual void bar();
virtual void baz();
};
struct B : A {
void foo();
void bar();
};
struct C : B {
void foo();
};
int main() {
B b;
A& br = b;
397
Inheritance Polymorphic Inheritance
The overriding and base class functions can have covariant return types
• Both types must be single-level pointers or references to classes
• The referenced/pointed-to class in the base class function must be a direct or
indirect base class of the referenced/pointed-to class in the derived class
function
• The return type in the derived class function must be at most as cv-qualified
as the return type in the base class function
• Most of the time, the referenced/pointed-to class in the derived class
function is the derived class itself
398
Inheritance Polymorphic Inheritance
Example
struct Base {
virtual Base* foo();
virtual Base* bar();
};
399
Inheritance Polymorphic Inheritance
struct Base {
Base() { foo(); }
virtual void foo();
};
int main() {
Derived d; // On construction, Base::foo() is called
}
400
Inheritance Polymorphic Inheritance
Virtual Destructors
Derived objects can be deleted through a pointer to the base class
• Undefined behavior unless the destructor in the base class is virtual
• The destructor in a base class should either be protected and non-virtual or
public and virtual
#include <memory>
struct Base {
virtual ~Base() { };
};
int main() {
Base* b = new Derived();
delete b; // OK
}
401
Inheritance Polymorphic Inheritance
struct Base {
virtual void foo(int i);
virtual void bar();
};
402
Inheritance Polymorphic Inheritance
struct Base {
virtual void foo() final;
};
403
Inheritance Polymorphic Inheritance
404
Inheritance Polymorphic Inheritance
C++ allows abstract classes which cannot be instantiated, but used as a base class
• Any class which declares or inherits at least one pure virtual function is an
abstract class
• A pure virtual member function declaration contains the sequence = 0 after
the declarator and override/final specifiers
• Pointers and references to an abstract class can be declared
405
Inheritance Polymorphic Inheritance
Example
struct Base {
virtual void foo() = 0;
};
int main() {
Base b; // ERROR
Derived d;
Base& dr = d;
dr.foo(); // calls Derived::foo()
}
406
Inheritance Polymorphic Inheritance
struct Base {
virtual void foo() = 0;
};
407
Inheritance Polymorphic Inheritance
struct Base {
virtual ~Base() = 0;
};
Base::~Base() { }
int main() {
Base b; // ERROR
}
408
Inheritance Polymorphic Inheritance
struct Base {
virtual ~Base();
virtual void foo() = 0;
};
int main() {
std::unique_ptr<Base> b = std::make_unique<Derived>();
b->foo(); // calls Derived::foo()
409
Inheritance Conversions
dynamic_cast (1)
410
Inheritance Conversions
dynamic_cast (2)
Example
struct A {
virtual ~A() = default;
};
struct B : A {
void foo() const;
};
struct C : A {
void bar() const;
};
411
Inheritance Conversions
dynamic_cast (3)
dynamic_cast has a non-trivial performance overhead
• Notable impact if many casts have to be performed
• Alternative: Use a type enum in conjunction with static_cast
struct Base {
enum class Type {
Base,
Derived
};
Type type;
Base() : type(Type::Base) { }
Base(Type type) : type(type) { }
virtual ~Base();
};
412
Inheritance Conversions
dynamic_cast (4)
Example (continued)
break;
}
}
413
Inheritance Implementation of Polymorphic Inheritance
Vtables (1)
414
Inheritance Implementation of Polymorphic Inheritance
Vtables (2)
Example
struct Base {
Code Segment
virtual void foo(); Base::foo(): vtable for Base:
virtual void bar();
}; Instructions... Base::foo()
Base::bar(): Base::bar()
struct Derived : Base {
void foo() override; Instructions... vtable for Derived:
};
Derived::foo(): Derived::foo()
int main() { Instructions... Base::bar()
Base b;
Derived d;
Stack
Base& br = b; Derived d:
Base& dr = d;
vtable pointer
br.foo(); Base b:
dr.foo();
} vtable pointer
415
Inheritance Implementation of Polymorphic Inheritance
Performance Implications
416
Inheritance Inheritance Modes
Inheritance Modes
417
Inheritance Inheritance Modes
Semantics
• Public base class members are usable as public members of the derived class
• Protected base class members are usable as protected members of the
derived class
418
Inheritance Inheritance Modes
class A {
protected:
int a;
public:
int b;
};
class B : public A {
public:
void foo() {
return a + 42; // OK: a is usable as protected member of B
}
};
int main() {
B b;
b.b = 42; // OK: b is usable as public member of B
b.a = 42; // ERROR: a is not visible
}
419
Inheritance Inheritance Modes
Semantics
• Public base class members are usable as private members of the derived class
• Protected base class members are usable as private members of the derived
class
420
Inheritance Inheritance Modes
Example
class A {
protected:
A(int); // Constructor is protected for some reason
};
class C : private A {
public:
C() : A(42) { }
421
Inheritance Inheritance Modes
Semantics
• Public base class members are usable as protected members of the derived
class
• Protected base class members are usable as protected members of the
derived class
• Within the derived class and all further-derived classes, pointers and
references to a derived object may be used where a pointer or reference to
the base object is expected
422
Inheritance Inheritance Modes
class A {
protected:
int a;
public:
int b;
};
class B : protected A {
public:
void foo() {
return a + 42; // OK: a is usable as protected member of B
}
};
int main() {
B b;
b.b = 42; // ERROR: b is not visible
b.a = 42; // ERROR: a is not visible
}
423
Inheritance Multiple Inheritance
Multiple Inheritance
424
Inheritance Exceptions
Exceptions in C++
While transferring control up the call stack, C++ performs stack unwinding
• Properly cleans up all objects with automatic storage duration
• Ensures correct behavior e.g. of RAII classes
425
Inheritance Exceptions
Throwing Exceptions
#include <exception>
void foo(unsigned i) {
if (i == 42)
throw 42;
throw std::exception();
}
426
Inheritance Exceptions
Handling Exceptions
Exceptions are handled in try-catch blocks
• Exceptions that occur while executing the try-block can be handled in the
catch-blocks
• The parameter type of the catch-block determines which type of exception
causes the block to be entered
#include <exception>
void bar() {
try {
foo(42);
} catch (int i) {
/* handle exception */
} catch (const std::exception& e) {
/* handle exception */
}
}
427
Inheritance Exceptions
Usage Guidelines
428
Templates
Templates
429
Templates
Motivation
430
Templates Basic Templates
Templates
431
Templates Basic Templates
Example
class A { /* ... */ };
//------------------------------------------------------------
template <class T> // T is a type template parameter
class vector {
public:
/* ... */
void push_back(const T& element);
/* ... */
};
//------------------------------------------------------------
int main() {
vector<int> vectorOfInt; // int is substituted for T
vector<A> vectorOfA; // A is substituted for T
}
432
Templates Basic Templates
Template Syntax
433
Templates Basic Templates
434
Templates Basic Templates
public:
T& operator[](size_t i) {
assert(i < N);
return storage[i];
}
};
435
Templates Basic Templates
436
Templates Basic Templates
437
Templates Basic Templates
Using Templates
438
Templates Basic Templates
Template arguments for type template parameters must name a type (which may
be incomplete)
class A;
//------------------------------------------------------------
template <class T1, class T2 = int, class T3 = double>
class Foo { };
//------------------------------------------------------------
int main() {
Foo<int> foo1;
Foo<A> foo2;
Foo<A*> foo3;
Foo<int, A> foo4;
Foo<int, A, A> foo5;
}
439
Templates Basic Templates
440
Templates Basic Templates
Example
//------------------------------------------------------------
template <unsigned N>
class Foo { };
//------------------------------------------------------------
int main() {
Foo<42u> foo1; // OK: no conversion
Foo<42> foo2; // OK: numeric conversion
}
441
Templates Basic Templates
constexpr
#include <array>
//--------------------------------------------------------------
class Element { /* ... */ };
//--------------------------------------------------------------
class Foo {
static constexpr size_t numElements = 42;
constexpr size_t calculateBufferSize(size_t elements) {
return elements * sizeof(Element);
}
442
Templates Basic Templates
#include <array>
//--------------------------------------------------------------
template <class T, size_t N>
class MyArray { };
//--------------------------------------------------------------
template <template<class, size_t> class Array>
class Foo {
Array<int, 42> bar;
};
//--------------------------------------------------------------
int main() {
Foo<MyArray> foo1;
Foo<std::array> foo2;
}
443
Templates Basic Templates
public:
/* ... */
/* ... */
};
444
Templates Basic Templates
class A { };
//--------------------------------------------------------------
template <class T>
void swap(T& a, T& b) {
T tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}
//--------------------------------------------------------------
int main() {
A a1;
A a2;
swap<A>(a1, a2);
swap(a1, a2); // Also OK: Template arguments are deduced
}
445
Templates Basic Templates
namespace something::extremely::nested {
//--------------------------------------------------------------
template <class T, class R>
class Handle { };
//--------------------------------------------------------------
} // namespace something::extremely::nested
//--------------------------------------------------------------
template <typename T>
using Handle = something::extremely::nested::Handle<T, void*>;
//--------------------------------------------------------------
int main() {
Handle<int> handle1;
Handle<double> handle2;
}
446
Templates Basic Templates
447
Templates Basic Templates
Foo foo;
foo.printSize<Foo::ArrayType<int>>();
}
448
Templates Basic Templates
Template Instantiation
A function or class template by itself is not a type, an object, or any other entity
• No assembly is generated from a file that contains only template definitions
• A template specialization must be instantiated for any assembly to appear
Template instantiation
• Compiler generates an actual function or class for a template specialization
• Explicit instantiation: Explicitly request instantiation of a specific
specialization
• Implicit instantiation: Use a template specialization in a context that requires
a complete type
449
Templates Basic Templates
Explanation
• Explicit instantiations have to follow the one definition rule
• Generates assembly for the function specialization or class specialization and
all its member functions
• Template definition must be visible at the point of explicit instantiation
• Template definition and explicit instantiation should usually be placed in
implementation file
450
Templates Basic Templates
451
Templates Basic Templates
452
Templates Basic Templates
T bar();
};
//--------------------------------------------------------------
int main() {
A<int> a; // Instantiates only A<int>
int x = a.foo(32); // Instantiates A<int>::foo
453
Templates Basic Templates
Implicit instantiation
• Pro: Template can be used with any suitable type
• Pro: No unnecessary assembly is generated
• Con: Definition has to be provided in header
• Con: User of our templates has to compile them
Explicit instantiation
• Pro: Explicit instantiations can be compiled into library
• Pro: Definition can be encapsulated in source file
• Con: Limits usability of our templates
454
Templates Basic Templates
Instantiation Caveats
455
Templates Basic Templates
A(T value);
456
Templates Basic Templates
457
Templates Basic Templates
458
Templates Basic Templates
#include <concepts>
459
Templates Basic Templates
Constraints are extremely useful to make working with templates somewhat easier
• Unfortunately not used much in the standard library
• Should be used as much as possible in your code
• Any assumptions about template parameters should be made explicit
460
Templates Basic Templates
B* b; // B refers to A<T>::B
void foo();
void bar() {
foo(); // foo refers to A<T>::foo
}
};
461
Templates Basic Templates
Names that are members of templates are not considered to be types by default
• When using a name that is a member of a template outside of any template
declaration or definition
• When using a name that is not a member of the current instantiation within
a template declaration or definition
• If such a name should be considered as a type, the typename disambiguator
has to be used
462
Templates Basic Templates
struct A {
using MemberTypeAlias = float;
};
//--------------------------------------------------------------
template <class T>
struct B {
// no disambiguator required
using AnotherMemberTypeAlias = T::MemberTypeAlias;
// disambiguator required
typename T::MemberTypeAlias* ptr;
};
//--------------------------------------------------------------
int main() {
// value has type float
B<A>::AnotherMemberTypeAlias value = 42.0f;
}
463
Templates Basic Templates
464
Templates Basic Templates
Reference Collapsing
int main() {
Foo<int&&>::Trref x; // what is the type of x?
}
465
Templates Template Specialization
We may want to modify the behavior of templates for specific template arguments
• For example, a templated find method can employ different algorithms on
arrays (binary search) vs. linked lists (linear search)
466
Templates Template Specialization
Full Specialization
Defines a specific implementation for a full set of template arguments
• Has to appear after the declaration of the original template
• Syntax: template <> declaration
• Most types of templates can be fully specialized
467
Templates Template Specialization
Partial Specialization
Defines a specific implementation for a partial set of template arguments
• Has to appear after the declaration of the original template
• template < parameter-list > class name < argument-list >
• template < parameter-list > struct name < argument-list >
• Only class templates can be partially specialized
• Function overloads can simulate function template specialization
template <class C, class T>
class SearchAlgorithm {
void find (const C& container, const T& value) {
/* do linear search */
}
};
//--------------------------------------------------------------
template <class T>
class SearchAlgorithm<std::vector<T>, T> {
void find (const std::vector<T>& container, const T& value) {
/* do binary search */
}
};
468
Templates Template Argument Deduction
469
Templates Template Argument Deduction
470
Templates Template Argument Deduction
#include <memory>
//-----------------------------------------------------------
template <class T>
struct Foo {
Foo(T t);
};
//-----------------------------------------------------------
int main() {
Foo foo(12);
std::unique_ptr ptr = make_unique<int>(42);
}
471
Templates Placeholder Type Specifiers
#include <unordered_map>
//-----------------------------------------------------------
int main() {
std::unordered_map<int, const char*> intToStringMap;
472
Templates Placeholder Type Specifiers
// GOOD:
const auto** f4 = foo(); // auto is int
}
473
Templates Placeholder Type Specifiers
struct A {
const A& foo() { return *this; }
};
//-----------------------------------------------------------
int main() {
A a;
auto a1 = a.foo(); // BAD: auto is const A, copy
const auto& a2 = a.foo() // GOOD: auto is A, no copy
}
474
Templates Placeholder Type Specifiers
Explanation
• The identifiers in identifier-list are bound to the subobjects or
elements of the initializer
• Can bind to arrays, tuple-like types and accessible data members
• Very useful during iteration, especially over associative containers
475
Templates Placeholder Type Specifiers
Example
#include <utility>
//-----------------------------------------------------------
struct Foo {
float y;
long z;
};
//-----------------------------------------------------------
std::pair<int, long> bar();
//-----------------------------------------------------------
int main() {
Foo foo;
int array[4];
auto [a1, a2, a3, a4] = array; // copies array, a1 - a4 refer to copy
auto& [y, z] = foo; // y refers to foo.y, z refers to foo.z
auto [l, r] = bar(); // move-constructs pair p, l refers to p.first,
// r refers to p.second
}
476
Templates Variadic Templates
477
Templates Variadic Templates
printTuple(tuple);
printElements(1, 2, 3, 4);
}
478
Templates Variadic Templates
#include <iostream>
//-----------------------------------------------------------
void printElements() { }
//-----------------------------------------------------------
template <typename Head, typename... Tail>
void printElements(const Head& head, const Tail&... tail) {
std::cout << head;
printElements(tail...);
}
//-----------------------------------------------------------
int main() {
printElements(1, 2, 3.0, 3.14, 4);
}
479
Templates Variadic Templates
Semantics
• (E ◦ . . .) becomes E1 ◦ (. . . (En−1 ◦ En ))
• (. . . ◦ E ) becomes ((E1 ◦ E2 ) ◦ . . .) ◦ En
• (E ◦ . . . ◦ I) becomes E1 ◦ (. . . (En−1 ◦ (En ◦ I)))
• (I ◦ . . . ◦ E ) becomes (((I ◦ E1 ) ◦ E2 ) ◦ . . .) ◦ En
480
Templates Variadic Templates
481
Templates Template Metaprogramming
Template Metaprogramming
482
Templates Idioms
static_assert
483
Templates Idioms
Type Traits
Type traits compute information about types at compile time
• Simple form of template metaprogramming
• E.g. std::numeric_limits is a type trait
Standard Library I
485
Standard Library I Introduction
486
Standard Library I Introduction
487
Standard Library I Introduction
• I/O
• Input-/Output streams
• File streams
• String streams
• Threads
• Thread class
• (shared) mutexes
• futures
• And much more
• Localization
• Regex
• Atomics
• Filesystem support
• …
488
Standard Library I Optional, Pair, Tuple
std::optional (1)
489
Standard Library I Optional, Pair, Tuple
std::optional (2)
Usage of std::optional
• std::optional<T>, where T can be almost any type (no references or
arrays)
• Guarantees to not dynamically allocate any memory when being assigned a
value
• Internally implemented as an object with a member that can store a T value
and a boolean
Useful member functions
• has_value() or implicit conversion to bool: Check whether the optional
contains a value
• Dereference operators * and ->: Access or interact with the contained value
(undefined behavior if the optional is empty)
• value_or(): Return the contained value if the optional is non-empty, or a
default value otherwise
• reset(): Clear the optional
490
Standard Library I Optional, Pair, Tuple
std::optional (3)
491
Standard Library I Optional, Pair, Tuple
std::optional (4)
might_fail(3).has_value(); // true
might_fail(8).has_value(); // false
// Or even simpler:
std::optional<std::string> opt5 = might_fail(5)
if (opt5) { //contextual conversion to bool
opt5->size(); // 11
}
492
Standard Library I Optional, Pair, Tuple
std::optional (5)
might_fail(42).value_or("default"); // "default"
Clearing an optional
493
Standard Library I Optional, Pair, Tuple
std::pair
std::pair<T, U> is a template class that stores exactly one object of type T
and one of type U.
• Defined in the header <utility>
• Constructor takes object of T and U
• Pairs can also be constructed with std::make_pair()
• Objects can be accessed with first and second
• Can be compared for equality and inequality
• Can be compared lexicographically with ==, and <=>
494
Standard Library I Optional, Pair, Tuple
std::tuple
495
Standard Library I Optional, Pair, Tuple
std::tie()
Tuples can also contain values of reference type. They can be constructed with
std::tie().
• Can be used to easily “decompose” a tuple into existing variables
• Can also be used to quickly do lexicographic comparison on different objects
496
Standard Library I Optional, Pair, Tuple
497
Standard Library I Optional, Pair, Tuple
struct Rational {
long numerator;
long denominator;
};
498
Standard Library I Strings
std::string
499
Standard Library I Strings
Creating a std::string
The default constructor creates an empty string of length 0
std::string s;
s.size(); // == 0
std::string s = "null\0byte!";
std::cout << s << std::endl; // prints "null"
500
Standard Library I Strings
501
Standard Library I Strings
502
Standard Library I Strings
Comparing std::string
Usually, the standard relational operators are used for string comparisons
• ==, <=> perform lexicographical comparisons
• Can only compare full strings
Example
503
Standard Library I Strings
std::string Operations
504
Standard Library I Strings
std::string_view (1)
std::string_view (2)
Example
std::string s = "garbage garbage garbage interesting garbage";
// Or in place:
s_view.remove_prefix(24); // O(1)
s_view.remove_suffix(s_view.size() - 11); // O(1)
String Literals
Regular string literals do not handle null byte content correctly (see above)
• The standard library provides special literals (“suffixes”) to construct
std::string_view and std::string objects that deal with null bytes
correctly.
• To use them, you have to use
using namespace std::literals::string_view_literals or
using namespace std::literals::string_literals.
Example
507
Standard Library I Strings
508
Standard Library I Strings
509
Standard Library I Containers
510
Standard Library I Containers
std::vector
511
Standard Library I Containers
std::vector<bool>
512
Standard Library I Containers
Note: It is not possible to insert new elements this way! You can only update
existing ones.
513
Standard Library I Containers
Insert or remove elements anywhere with an iterator pointing at the element after
insertion, or the element to be erased respectively:
auto it = fib.begin(); it += 2;
fib.insert(it, 42); // fib is now 1, 1, 42, 2, 3
514
Standard Library I Containers
std::vector<ExpensiveToCopy> vec;
515
Standard Library I Containers
If the final size of a vector is already known, give the vector a hint to avoid
unnecessary reallocations:
std::vector<int> vec;
vec.reserve(1'000'000); //enough space for 1'000'000 elements is allocated
vec.capacity(); // == 1'000'000
vec.size(); // == 0, do not mix this up with capacity!
516
Standard Library I Containers
std::span (1)
517
Standard Library I Containers
std::span (2)
valuesRef.size(); // == 5
valuesRef.data() == values.data(); // true
valuesRef[1]; // == 2
518
Standard Library I Containers
std::unordered_map
Use std::unordered_map if you need a hash table and don’t need ordering
519
Standard Library I Containers
if (search != name_to_grade.end()) {
// Returns an iterator pointing to a pair!
search->first; // == "schmidt"
search->second; // == 5.0
}
520
Standard Library I Containers
std::unordered_map: Insertion
Update or insert elements like this. If it did not exist, the brackets operator will
insert a default-constructed value.
Note: The brackets operator has no const overload.
name_to_grade["moritz"]; // Entry {"moritz", 0.0} is inserted
// Entry {"michael", 0.0} is created, then value is set to 3.0
name_to_grade["michael"] = 3.0;
// Or simpler:
name_to_grade.insert({"mustermann", 3.7});
521
Standard Library I Containers
std::unordered_map: Removal
522
Standard Library I Containers
std::map (1)
523
Standard Library I Containers
std::map (2)
524
Standard Library I Containers
std::unordered_set
525
Standard Library I Containers
if (search != shopping_list.end()) {
// Returns an iterator pointing to the element!
*search; // == "milk"
}
Or with contains():
shopping_list.contains("bread"); // true
shopping_list.contains("blafasel"); // false
526
Standard Library I Containers
std::unordered_set: Insertion
result = shopping_list.insert("broccoli");
result.second; // true, "broccoli" was added
*result.first; // "broccoli", iterator points to newly inserted element
527
Standard Library I Containers
std::unordered_set: Removal
528
Standard Library I Containers
std::set (1)
529
Standard Library I Containers
std::set (2)
530
Standard Library I Containers
The standard library has an entire library for I/O operations. The main concept of
the I/O library is a stream.
• Streams are organized in a class hierarchy
• std::istream is the base class for input operations (e.g. operator>>)
• std::ostream is the base class for output operations (e.g. operator<<)
• std::iostream is a subclass of std::istream and std::ostream
• std::cin is an instance of std::istream that represents stdin
• std::cout is an instance of std::ostream that represent stdout
As for strings, streams are actually templates parametrized with a character type.
• std::istream is an alias for std::basic_istream<char>
• std::ostream is an alias for std::basic_ostream<char>
532
Standard Library I Streams and I/O
All streams are subclasses of std::basic_ios and have the following member
functions:
• good(), fail(), bad(): Checks if the stream is in a specific error state
• eof(): Checks if the stream has reached end-of-file
• operator bool(): Returns true if stream has no errors
int value;
if (std::cin >> value) {
std::cout << "value = " << value << std::endl;
} else {
std::cout << "error" << std::endl;
}
533
Standard Library I Streams and I/O
Input Streams
Input streams (std::istream) support several input functions:
• operator>>(): Reads a value of a given type from the stream, skips
leading whitespace
• operator>>() can be overloaded for own types as second argument to
support being read from a stream
• get(): Reads single or multiple characters until a delimiter is found
• read(): Reads given number of characters
534
Standard Library I Streams and I/O
Output Streams
535
Standard Library I Streams and I/O
String Streams
std::stringstream can be used when input and output should be written and
read from a std::string.
• Defined in the header <sstream>
• Is a subclass of std::istream and std::ostream
• Initial contents can be given in the constructor
• Contents can be extracted and set with str()
536
Standard Library I Streams and I/O
File Streams
The standard library defines several streams for file I/O in the <fstream> header:
std::ifstream input("input_file");
if (!input) { std::cout << "couldn't open input_file\n"; }
std::ofstream output("output_file");
if (!output) { std::cout << "couldn't open output_file\n"; }
// Read an int from input_file and write it to output_file
int value = -1;
if (!(input >> value)) {
std::cout << "couldn't read from file\n";
}
if (!(output << value)) {
std::cout << "couldn't write to file\n";
}
537
Standard Library I Streams and I/O
Disadvantage of Streams
Even though streams are nice to use, they should be avoided in many cases:
• Streams make heavy use of virtual functions and virtual inheritance which by
itself can sometimes be a significant performance overhead
• Streams respect the system’s locale settings (e.g. whether to use a period or
a comma for floating point numbers) which also makes them slow
• Especially parsing of integers is very inefficient
General rule: When input is typed in by a user, using streams is fine. When input
is read from files or generated automatically, better use OS-specific functions.
538
Standard Library II
Standard Library II
539
Standard Library II Function Objects
540
Standard Library II Function Objects
541
Standard Library II Function Objects
542
Standard Library II Function Objects
543
Standard Library II Function Objects
544
Standard Library II Function Objects
545
Standard Library II Function Objects
Example
546
Standard Library II Function Objects
547
Standard Library II Function Objects
548
Standard Library II Function Objects
549
Standard Library II Function Objects
Capture types
int main() {
int i = 0;
int j = 42;
550
Standard Library II Function Objects
int main() {
int i = 42;
i = 0;
int a = lambda1(); // a = 84
int b = lambda2(); // b = 42
}
551
Standard Library II Function Objects
struct Foo {
int i = 0;
void bar() {
auto lambda1 = [*this]() {return i + 42; };
auto lambda2 = [this](){ return i + 42; };
i = 42;
int a = lambda1(); // a = 42
int b = lambda2(); // b = 84
}
};
552
Standard Library II Function Objects
struct Foo {
int i = 0;
void bar() {
auto lambda1 = [&]() {return i + 42; };
auto lambda2 = [=](){ return i + 42; };
i = 42;
int a = lambda1(); // a = 84
int b = lambda2(); // b = 84
}
};
553
Standard Library II Function Objects
#include <memory>
int main() {
auto ptr = std::make_unique<int>(4);
int a = f2(); // 4
ptr.reset();
int b = f2(); // undefined behavior
}
554
Standard Library II Function Objects
Situation so far
• Functions are generally stateless
• State has to be kept in surrounding object, e.g. class instances
• Lambda expressions allow limited state-keeping
555
Standard Library II Function Objects
struct Adder {
int value;
556
Standard Library II Function Objects
std::function (1)
#include <functional>
//--------------------------------------------------------
int add2(int p){ return p + 2; }
//--------------------------------------------------------
int main() {
std::function<int(int)>adder = add2;
int a = adder(5); // a = 7
}
557
Standard Library II Function Objects
std::function (2)
#include <functional>
//--------------------------------------------------------
std::function<int()> getFunction(bool first){
int a = 14;
if (first)
return [=]() { return a; };
else
return [=]() { return 2 * a; };
}
//--------------------------------------------------------
int main() {
return getFunction(false)() + getFunction(true)(); // 42
}
558
Standard Library II Function Objects
560
Standard Library II The Algorithms Library
std::sort
#include <algorithm>
#include <vector>
//--------------------------------------------------------
int main() {
std::vector<unsigned> v = {3, 4, 1, 2};
std::sort(v.begin(), v.end()); // 1, 2, 3, 4
}
561
Standard Library II The Algorithms Library
#include <algorithm>
#include <vector>
//--------------------------------------------------------
int main() {
std::vector<unsigned> v = {3, 4, 1, 2};
std::sort(v.begin(), v.end(), [](unsigned lhs, unsigned rhs) {
return lhs > rhs;
}); // 4, 3, 2, 1
}
562
Standard Library II The Algorithms Library
563
Standard Library II The Algorithms Library
Searching
General semantics
• Search operations return iterators pointing to the result
• Unsuccessful operations are usually indicated by returning the last iterator
of a range [first, last)
564
Standard Library II The Algorithms Library
Searching - Unsorted
565
Standard Library II The Algorithms Library
std::find
Example
#include <algorithm>
#include <vector>
//--------------------------------------------------------
int main() {
std::vector<int> v = {2, 6, 1, 7, 3, 7};
566
Standard Library II The Algorithms Library
std::find_if
Example
#include <algorithm>
#include <vector>
//--------------------------------------------------------
int main() {
std::vector<int> v = {2, 6, 1, 7, 3, 7};
assert(res2 == vec.end());
}
567
Standard Library II The Algorithms Library
Searching - Sorted
568
Standard Library II The Algorithms Library
std::binary_search
#include <algorithm>
#include <vector>
//--------------------------------------------------------
int main() {
std::vector<int> v = {1, 2, 2, 3, 3, 3, 4};
569
Standard Library II The Algorithms Library
std::lower_bound
Returns iterator pointing to the first element >= the search value
#include <algorithm>
#include <vector>
//--------------------------------------------------------
int main() {
std::vector<int> v = {1, 2, 2, 3, 3, 3, 4};
570
Standard Library II The Algorithms Library
std::upper_bound
Returns iterator pointing to the first element > the search value
#include <algorithm>
#include <vector>
//--------------------------------------------------------
int main() {
std::vector<int> v = {1, 2, 2, 3, 3, 3, 4};
571
Standard Library II The Algorithms Library
std::equal_range
#include <algorithm>
#include <vector>
//--------------------------------------------------------
int main() {
std::vector<int> v = {1, 2, 2, 3, 3, 3, 4};
572
Standard Library II The Algorithms Library
Permutations
573
Standard Library II The Algorithms Library
std::iota
#include <numeric>
#include <memory>
//--------------------------------------------------------
int main() {
auto heapArray = std::make_unique<int[]>(5);
std::iota(heapArray.get(), heapArray.get() + 5, 2);
574
Standard Library II The Algorithms Library
std::next_permutation
#include <algorithm>
#include <vector>
//--------------------------------------------------------
int main() {
std::vector<int> v = {1, 2, 3};
575
Standard Library II The Algorithms Library
std::prev_permutation
#include <algorithm>
#include <vector>
//--------------------------------------------------------
int main() {
std::vector<int> v = {1, 3, 2};
576
Standard Library II The Algorithms Library
Additional Functionality
577
Standard Library II The Ranges Library
The ranges library provides components for dealing with ranges of elements
• Ranges provide an abstraction of the [first, last) iterator pairs we have
seen so far
• Formalized by the range concept in the <ranges> header
• We can iterate over the elements of a range in the same way as over the
elements of a container
578
Standard Library II The Ranges Library
#include <ranges>
#include <iostream>
#include <map>
int main() {
std::map<int, int> map{{1, 2}, {3, 4}};
Output
1
3
579
Standard Library II The Ranges Library
Range factories can be used to create some commonly used views without
constructing a dedicated container
• views::empty – An empty view
• views::single – A view that contains a single element
• views::iota – A view consisting of repeatedly incremented values
580
Standard Library II The Ranges Library
int main() {
auto square = [](auto x) { return x * x; };
Output
1
4
9
16
581
Standard Library II The Ranges Library
582
Standard Library II The Ranges Library
Example
#include <ranges>
#include <iostream>
#include <map>
int main() {
std::map<int, int> map{{1, 2}, {3, 4}};
// Functional syntax
for (auto key : std::views::reverse(std::views::keys(map)))
std::cerr << key << std::endl;
583
Standard Library II The Ranges Library
584
Standard Library II The Ranges Library
#include <ranges>
#include <iostream>
int main() {
auto numbers = {1, 2, 3, 4};
auto square = [](auto x) { return x * x; };
// Functional syntax
for (auto i : std::views::transform(numbers, square))
std::cout << i << std::endl;
585
Standard Library II The Ranges Library
586
Standard Library II The Random Library
587
Standard Library II The Random Library
588
Standard Library II The Random Library
#include <cstdint>
#include <random>
//--------------------------------------------------------
int main() {
std::mt19937 engine(42);
589
Standard Library II The Random Library
std::random_device
#include <cstdint>
#include <random>
//--------------------------------------------------------
int main() {
std::mt19937 engine(std::random_device()());
590
Standard Library II The Random Library
Distributions
591
Standard Library II The Random Library
std::uniform_int_distribution
Generates discrete uniform random numbers in range [a, b]
• Integer type specified as template parameter
• Constructed as uniform_int_distribution<T>(T a, T b)
• If not specified a defaults to 0 and b to the maximum value of T
• Numbers generated by operator()(Generator& g) where g is any
random number generator
#include <random>
//--------------------------------------------------------
int main() {
std::mt19937 engine(42);
std::uniform_int_distribution<int> dist(-2, 2);
int d1 = dist(engine); // d1 == -1
int d2 = dist(engine); // d2 == -2
}
592
Standard Library II The Random Library
std::uniform_real_distribution
Generates continuous uniform random numbers in range [a, b]
• Floating point type specified as template parameter
• Constructed as uniform_real_distribution<T>(T a, T b)
• If not specified a defaults to 0 and b to the maximum value of T
• Numbers generated by operator()(Generator& g) where g is any
random number generator
#include <random>
//--------------------------------------------------------
int main() {
std::mt19937 engine(42);
std::uniform_real_distribution<float> dist(-2, 2);
593
Standard Library II The Random Library
Seeding
594
Standard Library II The Random Library
Example
#include <random>
//--------------------------------------------------------
int main() {
// Use random device to seed generator
std::random_device rd;
// Use pseudo-random generator to get random numbers
std::mt19937 engine(rd());
// Use distribution to generate dice rolls
std::uniform_int_distribution<> dist(1, 6);
595
Standard Library II The Random Library
int randomDiceroll() {
return gen() % 6 + 1;
}
596
Concurrency in Modern Hardware
597
Concurrency in Modern Hardware
Concurrency
What is concurrency?
function foo() { ... }
function bar() { ... }
function main() {
t1 = startThread(foo)
t2 = startThread(bar)
In this example program, concurrency means that foo() and bar() are executed
at the same time.
• How does a CPU actually do this?
• How can concurrency be used to make your programs faster?
598
Concurrency in Modern Hardware
599
Concurrency in Modern Hardware Simultaneous Multi-Threading (SMT)
CPU core
store add sub
T1 : CU1 ALU1 SIMD
µ1
1 µ1
2 µ2
1 µ2
2 µ3
1 µ3
2
µ2
2 µ4
2
sub
add store
time
padd add mul
600
Concurrency in Modern Hardware Simultaneous Multi-Threading (SMT)
When using SMT, multiple instruction streams share parts of the CPU core.
• When one stream alone already utilizes all computation units, SMT does not
increase performance
• Same for memory bandwidth
• Some units may only exist once on the core, so SMT can also decrease
performance
• When two threads from unrelated processes run on the same core, this can
potentially lead to security issues → Security issues similar to Spectre and
Meltdown are suspected to be enabled by SMT
601
Concurrency in Modern Hardware Cache Coherence
Cache Coherence
CPU 1 CPU 2
Main
Memory
L1-I L1-D L1-I L1-D
Unified L2 Unified L2
Unified L3
• Different cores can access the same memory at the same time
• Multiple cores potentially share caches
• Caches can be inclusive
• CPU must make sure that caching is consistent even with concurrent
accesses → Communication between CPUs with a Cache Coherence Protocol
602
Concurrency in Modern Hardware Cache Coherence
MESI Protocol
• CPUs and caches always read and write at cache line granularity, i.e. 64 byte
• The common MESI cache coherence protocol assigns every cache line one of
the four states:
• Modified: Cache line is stored in exactly one cache and was modified in the
cache but not yet written back to main memory
• Exclusive: Cache line is stored in exactly one cache to be used exclusively by
one CPU
• Shared: Cache line is stored in at least one cache, is currently used by a CPU
for read-only access, and was not modified, yet
• Invalid: Cache line is not loaded or being used exclusively by another cache
603
Concurrency in Modern Hardware Cache Coherence
Consider the following example program where foo() and bar() will be executed
concurrently:
globalCounter = 0
function foo() {
repeat 1000 times:
globalCounter = globalCounter - 1
}
function bar() {
repeat 1000 times:
globalCounter = globalCounter + 1
}
606
Concurrency in Modern Hardware Memory Order
Memory Order
607
Concurrency in Modern Hardware Memory Order
• CPU architectures usually have either weak memory order (e.g. ARM) or
strong memory order (e.g. x86)
• Weak Memory Order:
• As long as dependencies are respected, memory instructions and their effects
can be reordered
• Different threads will see writes in different orders
• Strong Memory Order:
• Within a thread, only stores are allowed to be delayed after subsequent loads,
everything else is not reordered
• When two threads execute stores to the same location, all other threads will
see the resulting writes in the same order
• Writes from a set of threads will be seen in the same order by all other threads
• For both:
• Writes from other threads can be reordered
• Concurrent memory accesses to the same location can be reordered
608
Concurrency in Modern Hardware Memory Order
In this example, initially the memory at A contains the value 1, the memory at B
the value 2.
Thread 1 Thread 2
store $3, A store $4, B
Thread 3 Thread 4
load A, %r1 load B, %r3
load B, %r2 load A, %r4
609
Concurrency in Modern Hardware Memory Order
8. write A
1. write A 5. write B
4. write B
Thread 3 Thread 4
load A, %r1 load B, %r3
load B, %r2 load A, %r4
2. read A → 3 6. read B → 4
3. read B → 2 7. read A → 1
Memory Barriers
• Multi-core CPUs have special memory barrier (also called memory fence)
instructions that can enforce stricter memory orders requirements
• This is especially useful for architectures with weak memory order
• x86 has the following barrier instructions:
• lfence: Earlier loads cannot be reordered beyond this instruction, later loads
and stores cannot be reordered before this instruction
• sfence: Earlier stores cannot be reordered beyond this instruction, later
stores cannot be reordered before this instruction
• mfence: No loads or stores can be reordered beyond or before this instruction
• ARM has the data memory barrier instruction that supports different modes:
• dmb ishst: All writes visible in or caused by this thread before this instruction
will be visible to all threads before any writes from stores after this instruction
• dmb ish: All writes visible in or caused by this thread and dependent reads
before this instruction will be visible to all threads before any reads and writes
after this instruction
• To additionally control out-of-order execution, ARM has the data
synchronization barrier instructions: dsb ishst, dsb ish
611
Concurrency in Modern Hardware Atomic Operations
Atomic Operations
612
Concurrency in Modern Hardware Atomic Operations
Because CAS operations can fail, they are usually used in a loop with the
following steps:
1. Load value from memory location into local register
2. Do computation with the local register assuming that no other thread will
modify the memory location
3. Generate new desired value for the memory location
4. Do a CAS operation on the memory location with the value in the local
register as expected value
5. Start the loop from the beginning if the CAS operation fails
Note that steps 2 and 3 can contain any number of instructions and are not
limited to RMW instructions!
614
Concurrency in Modern Hardware Atomic Operations
success = false
while (not success) { (Step 5)
expected = load(A) (Step 1)
desired = non_trivial_operation(expected) (Steps 2, 3)
success = CAS(A, expected, desired) (Step 4)
}
615
Parallel Programming
Parallel Programming
616
Parallel Programming
Parallel Programming
617
Parallel Programming Mutual Exclusion
A B C B C D
Final state
A B C D
Observations
• C is not actually removed
• Threads might also deallocate node memory after removal
618
Parallel Programming Mutual Exclusion
619
Parallel Programming Mutual Exclusion
Locks
620
Parallel Programming Mutual Exclusion
Shared Locks
621
Parallel Programming Mutual Exclusion
Deadlocks
• Multiple threads each wait for the other threads to release a lock
Thread 1 Thread 2
lock(A) lock(B)
Avoiding deadlocks
• If possible, threads should never acquire multiple locks
• If not avoidable, locks must always be acquired in a globally consistent order
622
Parallel Programming Mutual Exclusion
Starvation
• High contention on a mutex may lead to some threads making no progress
• Can partially be alleviated by using less restrictive locking schemes
High latency
• Some threads are blocked for a long time if a mutex is highly contended
• Can lead to noticeably reduced system performance
• Performance can possibly even drop below single-threaded performance
Priority inversion
• A high-priority thread may be blocked by a low-priority thread
• Due to the priority differential, the low-priority thread may not be allowed
sufficient compute resources to quickly release the lock
623
Parallel Programming Hardware-Assisted Synchronization
Hardware-Assisted Synchronization
For this reason, mutexes are best suited for coarse-grained locking
• E.g. locking an entire data structure instead of parts of it
• Sufficient if only very few threads contend for locks on the mutex
• Sufficient if the critical section protected by the mutex is much more
expensive than a (potential) system call to acquire a lock
624
Parallel Programming Hardware-Assisted Synchronization
Often, read-only accesses to a resource are more common than write accesses
• Thus we should optimize for the common case of read-only access
• In particular, parallel read-only access by many threads should be efficient
• Shared locks are not well-suited for this (see previous slide)
625
Parallel Programming Hardware-Assisted Synchronization
writer(optLock) {
lockExclusive(optLock.mutex) // begin critical section
storeAtomic(optLock.version, optLock.version + 1)
reader(optLock) {
while(true) {
current = loadAtomic(optLock.version); // begin critical section
626
Parallel Programming Hardware-Assisted Synchronization
627
Parallel Programming Hardware-Assisted Synchronization
In many cases, strict mutual exclusion is not required in the first place
• E.g. parallel insertion into a linked list
• We do not care about the order of insertions
• We only need to guarantee that all insertions are reflected in the final state
threadSafePush(linkedList, element) {
while (true) {
head = loadAtomic(linkedList.head)
element.next = head
if (CAS(linkedList.head, head, element))
break;
}
}
628
Parallel Programming Hardware-Assisted Synchronization
Non-Blocking Algorithms
Algorithms or data structures that do not rely on locks are called non-blocking
• E.g. the threadSafePush function on the previous slide
• Synchronization between threads is usually achieved using atomic operations
• Enables more efficient implementations of many common algorithms and
data structures
629
Parallel Programming Hardware-Assisted Synchronization
Often problems can be avoided by making sure that only the same operation (e.g.
insert) is executed in parallel
• E.g. insert elements in parallel in a first step, and remove them in parallel in a
second step
630
Parallel Programming Hardware-Assisted Synchronization
threadSafePush(stack, element) {
while (true) {
head = loadAtomic(stack.head)
element.next = head
if (CAS(stack.head, head, element))
break;
}
}
threadSafePop(stack) {
while (true) {
head = loadAtomic(stack.head)
next = head.next
if (CAS(stack.head, head, next))
return head
}
}
631
Parallel Programming Hardware-Assisted Synchronization
Consider the following initial state of the stack, on which two threads perform
some operations in parallel
B C D
Thread 1 Thread 2
x = threadSafePop(stack) y = threadSafePop(stack)
z = threadSafePop(stack)
threadSafePush(stack, y)
632
Parallel Programming Hardware-Assisted Synchronization
B C D
Thread 1 Thread 2
633
Parallel Programming Hardware-Assisted Synchronization
B C D
Thread 1 Thread 2
head = loadAtomic(stack.head)
// head == B
next = head.next
// next == C
634
Parallel Programming Hardware-Assisted Synchronization
C D
Thread 1 Thread 2
head = loadAtomic(stack.head)
// head == B
next = head.next
// next == C
y = threadSafePop(stack)
// y == B
635
Parallel Programming Hardware-Assisted Synchronization
Thread 1 Thread 2
head = loadAtomic(stack.head)
// head == B
next = head.next
// next == C
y = threadSafePop(stack)
// y == B
z = threadSafePop(stack)
// z == C
636
Parallel Programming Hardware-Assisted Synchronization
B D
Thread 1 Thread 2
head = loadAtomic(stack.head)
// head == B
next = head.next
// next == C
y = threadSafePop(stack)
// y == B
z = threadSafePop(stack)
// z == C
threadSafePush(stack, y)
637
Parallel Programming Hardware-Assisted Synchronization
C D
Thread 1 Thread 2
head = loadAtomic(stack.head)
// head == B
next = head.next
// next == C
y = threadSafePop(stack)
// y == B
z = threadSafePop(stack)
// z == C
threadSafePush(stack, y)
CAS(stack.head, head, next)
// inconsistent state!
638
Parallel Programming Hardware-Assisted Synchronization
function lock(mutexAddress) {
while (CAS(mutexAddress, 0, 1) not sucessful) {
<noop>
}
}
function unlock(mutexAddress) {
atomicStore(mutexAddress, 0)
}
639
Parallel Programming Hardware-Assisted Synchronization
Using this CAS loop as a mutex, also called spin lock, has several disadvantages:
• It has no fairness, i.e. does not guarantee that a thread will acquire the lock
eventually → starvation
• The CAS loop consumes CPU cycles (waste of energy and resources)
• Can easily lead to priority inversion
• The scheduler of the operating system thinks that the spinning thread requires
a lot of CPU time
• The spinning thread actually does no useful work at all
• In the worst-case, the scheduler takes CPU time away from the thread that
holds the lock to give it to the spinning thread
→ Spinning thread needs to spin even longer which makes the situation worse
Possible solutions:
• Spin for a limited number of times (e.g. several hundred thousand iterations)
• If the lock could not be acquired, fall back to a “real” mutex
• This is actually already how mutexes are usually implemented
640
Multi-Threading in C++
Multi-Threading in C++
641
Multi-Threading in C++
Multi-Threading in C++
In C++ it is allowed to run multiple threads simultaneously that use the same
memory.
• Multiple threads may read from the same memory location
• All other accesses (i.e. read-write, write-read, write-write) are called conflicts
• Conflicting operations are only allowed when threads are synchronized
• This can be done with mutexes or atomic operations
• Unsynchronized accesses (also called data races), deadlocks, and other
potential issues when using threads are undefined behavior!
642
Multi-Threading in C++ Threads Library
cmake_minimum_required(VERSION 3.21)
project(sample)
find_package(Threads REQUIRED)
add_executable(sample main.cpp)
target_link_libraries(sample PUBLIC Threads::Threads)
643
Multi-Threading in C++ Threads Library
The member function join() must be used to wait for a thread to finish
• join() must be called exactly once for each thread
• join() must be called before an std::thread object is destroyed
• When the destructor of an std::thread is called, the program is
terminated if the associated thread was not joined
644
Multi-Threading in C++ Threads Library
Example
#include <thread>
main() t2 t1
t1 constructed
void foo(int a, int b);
t2 constructed
int main() {
foo(1, 2)
// Pass a function and args
foo(3, 4)
std::thread t1(foo, 1, 2);
foo(5, 6)
// Pass a lambda
std::thread t2([]() {
foo(3, 4);
});
foo(5, 6);
t2 joined
t2.join();
t1.join(); t1 joined
}
645
Multi-Threading in C++ Threads Library
#include <iostream>
#include <string_view>
#include <thread>
int main() {
{
std::thread t1([]() { safe_print("Hi\n"); });
t1.join();
}
// Everything is fine, we called t1.join()
{
std::thread t2([]() {});
}
// Program terminated because t2.join() was not called
}
646
Multi-Threading in C++ Threads Library
#include <iostream>
#include <string_view>
#include <thread>
int main() {
std::thread t1([]() { safe_print("Hi\n"); });
std::thread t2 = std::move(t1); // t1 is now empty
t2.join(); // OK, thread originally started in t1 is joined
}
647
Multi-Threading in C++ Threads Library
#include <thread>
#include <vector>
int main() {
std::vector<std::thread> threadPool;
for (int i = 1; i <= 9; ++i) {
threadPool.emplace_back([i]() { safe_print(i); });
}
// Digits 1 to 9 are printed (unordered)
for (auto& t : threadPool) {
t.join();
}
}
648
Multi-Threading in C++ Threads Library
The thread library also contains other useful functions that are closely related to
starting and stopping threads:
• std::this_thread::sleep_for(): Stop the current thread for a given
amount of time
• std::this_thread::sleep_until(): Stop the current thread until a
given point in time
• std::this_thread::yield(): Let the operating system schedule another
thread
• std::this_thread::get_id(): Get the (operating-system-specific) id of
the current thread
649
Multi-Threading in C++ Mutual Exclusion
650
Multi-Threading in C++ Mutual Exclusion
The standard library defines several useful classes that implement mutexes in the
<mutex> and <shared_mutex> headers
• std::mutex – regular mutual exclusion
• std::recursive_mutex – recursive mutual exclusion
• std::shared_mutex – mutual exclusion with shared locks
The standard library provides RAII wrappers for locking and unlocking mutexes
• std::unique_lock – RAII wrapper for exclusive locking
• std::shared_lock – RAII wrapper for shared locking
The RAII wrappers should always be preferred for locking and unlocking mutexes
• Makes bugs due to inconsistent locking/unlocking much more unlikely
• Manual locking and unlocking may be required in some rare cases
• Should still be performed through the corresponding functions of the RAII
wrappers
651
Multi-Threading in C++ Mutual Exclusion
std::unique_lock (1)
#include <mutex>
#include <iostream>
std::mutex printMutex;
void safe_print(int i) {
std::unique_lock lock(printMutex); // lock is acquired
std::cout << i;
} // lock is released
652
Multi-Threading in C++ Mutual Exclusion
std::unique_lock (2)
653
Multi-Threading in C++ Mutual Exclusion
std::unique_lock (3)
Example
#include <mutex>
std::mutex mutex;
void foo() {
std::unique_lock lock(mutex, std::try_to_lock);
if (!lock) {
doUnsynchronizedWork();
doSynchronizedWork();
doUnsynchronizedWork();
}
654
Multi-Threading in C++ Mutual Exclusion
std::unique_lock (4)
std::unique_lock is movable to transfer ownership of a lock on a mutex
#include <mutex>
class MyContainer {
private:
std::mutex mutex;
public:
class iterator { /* ... */ };
iterator begin() {
std::unique_lock lock(mutex);
#include <mutex>
std::mutex mutex;
void bar() {
std::unique_lock lock(mutex);
// do some work...
}
void foo() {
std::unique_lock lock(mutex);
// do some work...
656
Multi-Threading in C++ Mutual Exclusion
#include <mutex>
std::recursive_mutex mutex;
void bar() {
std::unique_lock lock(mutex);
}
void foo() {
std::unique_lock lock(mutex);
bar(); // OK, will not deadlock
}
657
Multi-Threading in C++ Mutual Exclusion
std::shared_lock (1)
658
Multi-Threading in C++ Mutual Exclusion
std::shared_lock (2)
Example
#include <shared_mutex>
class SafeCounter {
private:
mutable std::shared_mutex mutex;
size_t value = 0;
public:
size_t getValue() const {
std::shared_lock lock(mutex);
return value; // read access
}
void incrementValue() {
std::unique_lock lock(mutex);
++value; // write access
}
};
659
Multi-Threading in C++ Mutual Exclusion
Using mutexes without care can easily lead to deadlocks within the system
• Usually occurs when a thread tries to lock another mutex when it already
holds a lock on some mutex
• Can in some cases be avoided by using std::recursive_mutex (if we are
locking the same mutex multiple times)
• Requires dedicated programming techniques when multiple mutexes are
involved
660
Multi-Threading in C++ Mutual Exclusion
662
Multi-Threading in C++ Mutual Exclusion
663
Multi-Threading in C++ Mutual Exclusion
664
Multi-Threading in C++ Mutual Exclusion
665
Multi-Threading in C++ Mutual Exclusion
One use case for condition variables are worker queues: Tasks are inserted into a
queue and then worker threads are notified to do the task.
666
Multi-Threading in C++ Atomic Operations
Atomic Operations
667
Multi-Threading in C++ Atomic Operations
668
Multi-Threading in C++ Atomic Operations
669
Multi-Threading in C++ Atomic Operations
#include <thread>
int main() {
unsigned value = 0;
std::thread t([&]() {
for (size_t i = 0; i < 10; ++i)
++value; // UNDEFINED BEHAVIOR, data race
});
t.join();
670
Multi-Threading in C++ Atomic Operations
#include <atomic>
#include <thread>
int main() {
std::atomic<unsigned> value = 0;
std::thread t([&]() {
for (size_t i = 0; i < 10; ++i)
value.fetch_add(1); // OK, atomic increment
});
t.join();
671
Multi-Threading in C++ Atomic Operations
C++ may support atomic operations that are not supported by the CPU
• std::atomic<T> can be used with any trivially copyable type
• In particular also for types that are much larger than one cache line
• To guarantee atomicity, compilers are allowed to fall back to mutexes
672
Multi-Threading in C++ Atomic Operations
673
Multi-Threading in C++ Atomic Operations
std::atomic<int> i = 0, j = 0;
void workerThread() {
i.fetch_add(1); // (A)
i.fetch_sub(1); // (B)
j.fetch_add(1); // (C)
}
void readerThread() {
int iLocal = i.load(), jLocal = j.load();
assert(iLocal != -1); // always true
}
Observations
• Reader threads will never see a modification order with (B) before (A)
• Depending on the memory order, multiple reader threads may see any of
(A),(B),(C), or (A),(C),(B), or (C),(A),(B)
674
Multi-Threading in C++ Atomic Operations
std::atomic<int> i = 0;
675
Multi-Threading in C++ Atomic Operations
std::atomic<int> i = 0, j = 0;
void threadA() {
while (true) {
i.fetch_add(1, std::memory_order_relaxed); // (A)
i.fetch_sub(1, std::memory_order_relaxed); // (B)
j.fetch_add(1, std::memory_order_relaxed); // (C)
}
}
void threadB() { /* ... */ }
void threadC() { /* ... */ }
Observations
• threadB() may observe (A),(B),(C)
• threadC() may observe (C),(A),(B)
676
Multi-Threading in C++ Atomic Operations
Observations
• threadB() may observe (C),(A),(B)
• threadC() will then also observe (C),(A),(B)
677
Multi-Threading in C++ Atomic Operations
Often the main building block to synchronize data structures without mutexes
• Allows us to check that no modifications occurred to an atomic over some
time period
• Can be used to implement “implicit” mutual exclusion
• Can suffer from subtle problems such as the A-B-A problem
678
Multi-Threading in C++ Atomic Operations
class SafeList {
private:
struct Entry {
T value;
Entry* next;
};
std::atomic<Entry*> head;
public:
void insert(const T& value) {
auto* entry = allocateEntry(value);
auto* currentHead = head.load();
do {
entry->next = currentHead;
} while (!head.compare_exchange_weak(currentHead, entry));
}
};
679
Multi-Threading in C++ Atomic Operations
std::atomic actually provides two CAS versions with the same signature
• compare_exchange_weak – weak CAS
• compare_exchange_strong – strong CAS
Semantics
• The weak version is allowed to return false, even when no other thread
modified the value
• This is called “spurious failure”
• The strong version may use a loop internally to avoid this
• General rule: If you use a CAS operation in a loop, always use the weak
version
680
Multi-Threading in C++ Atomic Operations
std::atomic_ref (1)
681
Multi-Threading in C++ Atomic Operations
std::atomic_ref (2)
Example
#include <atomic>
#include <thread>
#include <vector>
int main() {
std::vector<int> localCounters(4);
std::vector<std::thread> threads;
683
Organizing Larger Projects
Overview
This lecture attempts to give some suggestions and an overview of useful tools
• Project layout suggestions (tailored to CMake)
• Integrating third-party tools and libraries with CMake
• Advanced debugging facilities
• We do not claim completeness or bias-free presentation
• Refer to the CMake documentation for much more detail
684
Organizing Larger Projects Project Layout
Changes to one of these properties likely entail changes to the other properties
• Namespace structure should (roughly) reflect directory structure and
vice-versa
• Different libraries and executables ideally reside in separate source trees (i.e.
directories)
685
Organizing Larger Projects Project Layout
General guidelines
• Always clearly organize files, directories and namespaces with modularization
in mind
• Start with a monolithic library/executable structure and move to a more
independent and modular structure as the project grows
686
Organizing Larger Projects Project Layout
Directory Structure
687
Organizing Larger Projects Project Layout
Evolution
• Eventually, some library or executable in a small project will grow large
• Should then be moved into an independent (sub-)project
688
Organizing Larger Projects Project Layout
689
Organizing Larger Projects Project Layout
Evolution
• Eventually other projects or people may want to reuse one of the subprojects
in a different context
• Should then be moved into an entirely independent project
690
Organizing Larger Projects Project Layout
691
Organizing Larger Projects Project Layout
File content
• Generally, there should be one separate pair of header and implementation
files for each C++ class
• Very tightly coupled classes (e.g. classes that could also be nested classes)
can be placed in the same header and implementation files
File location
• Option 1: Place associated implementation and header files in the same
directory (preferred by us)
• Option 2: Place associated implementation and header files in separate
directory trees (e.g. src and include)
• Option 1 makes browsing code somewhat easier, option 2 makes system-wide
installation easier
692
Organizing Larger Projects Project Layout
693
Organizing Larger Projects Project Layout
694
Organizing Larger Projects Project Layout
Include Directories
Usually, the include path for a library should contain a prefix
• E.g. includes for a library “foo” could start with #include "foo/..."
• Requires a suitable directory structure in the source tree of the library
• Usually requires the use of target_include_directories in the
CMakeLists.txt
695
Organizing Larger Projects Libraries & Executables
697
Organizing Larger Projects Libraries & Executables
set(MY_EXECUTABLE_SOURCES
src/my_executable/Helper.cpp
...
src/my_executable/Main.cpp
)
add_executable(my_executable ${MY_EXECUTABLE_SOURCES})
698
Organizing Larger Projects Libraries & Executables
Static Libraries
699
Organizing Larger Projects Libraries & Executables
Shared Libraries
700
Organizing Larger Projects Libraries & Executables
Advantages
• Can have slightly higher performance since there are no indirections
• Can prevent compatibility issues since there are no external dependencies
Disadvantages
• Much bigger file sizes than shared libraries since code is actually copied
• Programs depending on static libraries have to be recompiled if the static
library changes
• Can lead to problems with transitive dependencies even if they are “header
only”
701
Organizing Larger Projects Libraries & Executables
Advantages
• Much smaller file sizes since the shared library is only loaded into memory at
run time
• Much lower memory consumption since only a single copy of a shared library
is kept in memory (even for unrelated processes)
• Can be exchanged for other compatible versions without changing programs
that depend on a shared library
Disadvantages
• Programs depending on a shared library rely on a compatible version being
available
• Can be slightly slower due to additional indirection at runtime
702
Organizing Larger Projects Libraries & Executables
703
Organizing Larger Projects Libraries & Executables
set(MY_LIBRARY_SOURCES
src/my_library/ClassA.cpp
...
src/my_library/ClassZ.cpp
)
704
Organizing Larger Projects Libraries & Executables
705
Organizing Larger Projects Libraries & Executables
set(MY_LIBRARY_SOURCES
src/my_library/ClassA.cpp
...
src/my_library/ClassZ.cpp
)
706
Organizing Larger Projects Libraries & Executables
Usually only the implementation files (*.cpp) should be added to a CMake target
• Header files on their own are not compiled
• Only headers that are included by implementation files are relevant for
compilation
707
Organizing Larger Projects Libraries & Executables
add_library(my_library INTERFACE)
target_include_directories(my_library INTERFACE src)
target_link_libraries(my_library INTERFACE some_dependency)
708
Organizing Larger Projects Libraries & Executables
709
Organizing Larger Projects Libraries & Executables
cmake_minimum_required(VERSION 3.12)
project(project)
add_subdirectory(my_executable)
add_subdirectory(my_library)
710
Organizing Larger Projects Libraries & Executables
711
Organizing Larger Projects Libraries & Executables
712
Organizing Larger Projects Libraries & Executables
set(MY_EXECUTABLE_SOURCES
src/my_executable/Helper.cpp
...
src/my_executable/Main.cpp
)
add_executable(my_executable ${MY_EXECUTABLE_SOURCES})
# allows includes to be '#include "my_executable/..."
# instead of '#include "my_executable/src/my_executable/..."
target_include_directories(my_executable PRIVATE src/)
# dependency on the my_libary target defined in other subproject
target_link_libraries(my_executable PRIVATE my_library)
713
Organizing Larger Projects Libraries & Executables
Paths in CMake
CMake defines several variables for often-used paths
CMAKE_SOURCE_DIR
Contains the full path to the top level of the source tree, i.e. the location of the
top-level CMakeLists.txt
CMAKE_CURRENT_SOURCE_DIR
Contains the full path the the source directory that is currently being processed
by CMake. Differs from CMAKE_SOURCE_DIR in directories added through
add_subdirectory.
CMAKE_BINARY_DIR
Contains the full path to the top level of the build tree, i.e. the build directory in
which cmake is invoked.
CMAKE_CURRENT_BINARY_DIR
Contains the full path the binary directory that is currently being processed.
Each directory added through add_subdirectory will create a corresponding
binary directory in the build tree.
Relative paths are usually relative to the current source directory
714
Organizing Larger Projects Third-Party Libraries
Third-Party Libraries
If possible and feasible, your project should not bundle third-party dependencies
• Many libraries can easily be installed through a package manager
• Reduces complexity of project configuration and maintenance
• CMake provides facilities for locating third-party dependencies in a
platform-independent way
715
Organizing Larger Projects Third-Party Libraries
find_package (1)
716
Organizing Larger Projects Third-Party Libraries
find_package (2)
Example
...
add_executable(tester ...)
target_link_libraries(tester PRIVATE
...
GTest::GTest # Imported target for the gtest library
# as specified by the documentation of
# FindGTest
)
717
Organizing Larger Projects Third-Party Libraries
find_library (1)
718
Organizing Larger Projects Third-Party Libraries
find_library (2)
Example (assuming there is no FindGTest.cmake script)
...
add_executable(tester ...)
target_link_libraries(tester PRIVATE
...
GTest # Only adds the libgtest library
# Does not set include paths
)
719
Organizing Larger Projects Third-Party Libraries
Further Reading
720
Organizing Larger Projects Testing
Testing
721
Organizing Larger Projects Testing
Googletest (1)
Functionality overview
• Test cases
• Predefined and user-defined assertions
• Death tests
• …
722
Organizing Larger Projects Testing
Googletest (2)
Simple tests
#include <gtest/gtest.h>
//--------------------------------------------------------
TEST(TestSuiteName, TestName) {
...
}
723
Organizing Larger Projects Testing
Googletest (3)
Fatal assertions
• Fatal assertions are prefixed with ASSERT_
• When a fatal assertion fails the test function is immediately terminated
Non-fatal assertions
• Non-fatal assertions are prefixed with EXPECT_
• When a non-fatal assertion fails the test function is allowed to continue
• Nevertheless the test case will fail
• All assertions exist in fatal and non-fatal versions
Assertion examples
• ASSERT_TRUE(condition); or ASSERT_FALSE(condition);
• ASSERT_EQ(val1, val2); or ASSERT_NE(val1, val2);
• …
724
Organizing Larger Projects Testing
Googletest (4)
#include <gtest/gtest.h>
//--------------------------------------------------------
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
725
Organizing Larger Projects Testing
Coverage (1)
726
Organizing Larger Projects Testing
Coverage (2)
Brief example
727
Organizing Larger Projects Further Tools & Techniques
Continuous Integration
728
Organizing Larger Projects Further Tools & Techniques
Linting
729
Organizing Larger Projects Further Tools & Techniques
perf (1)
730
Organizing Larger Projects Further Tools & Techniques
perf (2)
perf stat example
> perf stat --detailed ./my_executable
...
Performance counter stats for './my_executable':
Valgrind
Use cases
• Complex memory bugs that are not detected by simpler tools like the address
sanitizer
• Complex profiling tasks
732
Organizing Larger Projects Further Tools & Techniques
Regular debuggers like GDB can only step forward in the program
• Does not necessarily fit debugging requirements
• E.g. when a crash occurs, we would like to step backwards until we have
found the source of the crash
733
Organizing Larger Projects Further Tools & Techniques
void bar() {
assert((a % 2) == 0);
a = (a + 2) % max;
}
};
//--------------------------------------------------------
int main() {
Foo foo;
for (unsigned i = 0; i < 16; ++i)
foo.bar();
}
734
Organizing Larger Projects Further Tools & Techniques
Old value = 1
New value = 14
0x00005568dba67208 in Foo::bar (this=0x7fff75f38980) at main.cpp:9
9 a = (a + 2) % max;
735
C++ Systems Programming on Linux
736
C++ Systems Programming on Linux
Until now, most topics were about standard C++. The standard does not contain
everything that is useful for good systems programming, such as:
• Creating, removing, renaming files and directories
• Efficient reading and writing of files
• Direct manual memory allocation from the kernel
• Networking
• Management of processes and threads
The Linux kernel in particular has a very extensive user-space C-API that can be
used to directly communicate with the kernel for all of those tasks.
737
C++ Systems Programming on Linux
738
C++ Systems Programming on Linux Interacting with Files
File Descriptors
A very central concept in the POSIX API are so called file descriptors (fds).
• File descriptors have the type int
• They are used as a “handle” to:
• Files in the filesystem
• Directories in the filesystem
• Network sockets
• Many other kernel objects
• Usually, fds are created by a function (e.g. open()) and must be closed by
another function (e.g. close())
• When working with fds in C++, the RAII pattern can be very useful
739
C++ Systems Programming on Linux Interacting with Files
To open and create files the open() function can be used. It must be included
from <sys/stat.h> and <fcntl.h>.
• int open(const char* path, int flags, mode_t mode)
• Opens the file at path with the given flags and returns an fd for that file
• If an error occurs, -1 is returned
• The third argument mode is optional and only required when a file is created
• flags is a bitmap (created with bitwise or) that must contain exactly one of
the following flags:
O_RDONLY Open the file only for reading.
O_RDWR Open the file for reading and writing.
O_WRONLY Open the file only for writing.
• close() must be used to close the fd returned by open() → RAII
740
C++ Systems Programming on Linux Interacting with Files
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
int main() {
int fd = open("/tmp/testfile", O_WRONLY | O_CREAT, 0600);
if (fd < 0) { /* error */ }
else { close(fd); }
}
741
C++ Systems Programming on Linux Interacting with Files
To read from and write to files, read() and write() from the header
<unistd.h> can be used.
• ssize_t read(int fd, void* buf, size_t count)
• ssize_t write(int fd, const void* buf, size_t count)
• fd must be a valid file descriptor
• buf must be a memory buffer which has a size of at least count bytes
• The return value indicates how many bytes were actually read or written (can
be up to count)
• Both functions return -1 when an error occurs
• Note: Both functions may wait until data can actually be read or written
which can lead to deadlocks!
742
C++ Systems Programming on Linux Interacting with Files
For an opened file the kernel remembers the current position in the file.
• read() and write() start reading or writing from the current position
• They both advance the current position by the number of bytes read or
written
The function lseek() (headers <sys/types.h> and <unistd.h>) can be used
to get or set the current position.
• off_t lseek(int fd, off_t offset, int whence)
• off_t is a signed integer type
• The current position is changed according to offset and whence, which is
one of the following:
SEEK_SET The current position is set to offset
SEEK_CUR offset is added to the current position
SEEK_END The current position is set to the end of the file plus offset
• lseek() returns the value of the new position, or -1 if an error occurred
743
C++ Systems Programming on Linux Interacting with Files
Example:
Note: The current position is shared between all threads. Generally, read(),
write(), and lseek() should not be used concurrently on the same fd.
744
C++ Systems Programming on Linux Interacting with Files
There also exist two functions that read or write from a file without using the
current position: pread() and pwrite() from the header <unistd.h>.
• ssize_t pread(int fd, void* buf, size_t count, off_t offset)
• ssize_t pwrite(int fd, const void* buf, size_t count, off_t offset)
• Conceptually, those functions work like lseek(fd, offset, SEEK_SET)
followed by read() or write()
• However, they do not modify the current position in the file
• Should be used when reading from and writing to files from multiple threads
745
C++ Systems Programming on Linux Interacting with Files
Meta data of files, such as the type of a file, its size, its owner, or the date it was
last modified, can be read with stat() or fstat(). Required headers:
<sys/types.h>, <sys/stat.h>, <unistd.h>.
• int stat(const char* filename, struct stat* statbuf)
• int fstat(int fd, struct stat* statbuf)
• The meta data of the file specified by filename or fd is written into
statbuf
• Returns 0 on success, -1 on error
• struct stat has several member variables:
mode_t st_mode The file mode (S_IFREG for regular file, S_IFDIR for
directory, S_IFLNK for symbolic link, …)
uid_t st_uid The user id of the owner
off_t st_size The total size in bytes
…
746
C++ Systems Programming on Linux Interacting with Files
747
C++ Systems Programming on Linux Interacting with Files
POSIX and Linux have many more functions that deal with files and directories:
mkdir() Create a directory
mkdirat() Create a subdirectory in a specific directory
openat() Open a file in a specific directory
unlink() Remove a file
unlinkat() Remove a file from a specific directory
rmdir() Remove an empty directory
chmod()/fchmod() Change the permissions of a file
chown()/fchown() Change the owner of a file
fsync() Force changes to a file to be written
…
748
C++ Systems Programming on Linux Memory Mapping
Memory Mapping
POSIX defines the function mmap() in the header <sys/mman.h> which can be
used to manage the virtual address space of a process.
• void* mmap(void* addr, size_t length, int prot, int flags,
int fd, off_t offset)
• Arguments have different meaning depending on flags
• On error, the special value MAP_FAILED is returned
• Always: If a pointer is returned successfully, it must be freed with munmap()
• int munmap(void* addr, size_t length)
• addr must be a value returned from mmap()
• length must be the same value passed to mmap()
• RAII should be used to ensure that munmap() is called
749
C++ Systems Programming on Linux Memory Mapping
• Note: This assumes that integers are written in binary format to the file!
• Using mmap() to read from large files is often faster than using read()
• This is because with mmap() data is directly read from and written to the file
without copying it to a buffer first
751
C++ Systems Programming on Linux Memory Mapping
752
C++ Systems Programming on Linux Process Management
One possible output for this example is: start old end new end
753
C++ Systems Programming on Linux Process Management
756
C++ Systems Programming on Linux Process Management
Thread Pinning
Threads can control on which physical CPU cores they run by using
sched_setaffinity() from <sched.h>.
• int sched_setaffinitiy(pid_t pid, size_t cpusetsize
const cpu_set_t* mask)
• pid stands for the process id whose affinity should be set, or 0 which stands
for the current thread
• cpusetsize must be set to sizeof(cpu_set_t)
• mask is a pointer to a cpu_set_t which describes which CPU cores the
thread is allowed to run on
• Returns 0 on success, -1 on error
• Variables of type cpu_set_t can be modified with
CPU_ZERO(cpu_set_t* set) and
CPU_SET(int cpu, cpu_set_t* set)
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(0, &set); CPU_SET(4, &set);
sched_setaffinity(0, sizeof(cpu_set_t), &set);
757
C++ Systems Programming on Linux Process Management
Signals
758
C++ Systems Programming on Linux Process Management
Signal handlers can set by using sigaction() from the header <signal.h>.
• int sigaction(int signum, const struct sigaction* act,
struct sigaction* sigact)
• signum is the signal whose signal handler should be changed
• act is a pointer to the signal handler that should be set, or nullptr if an
existing signal handler should be removed
• If sigact is not nullptr, it will contain the old signal handler after the
function returns
• Returns 0 on success, -1 on error
• struct sigaction has several members, the most important one is:
void (*sa_handler)(int)
• sa_handler is a function pointer that points to the signal handler function
that takes the signal as only argument
759
C++ Systems Programming on Linux Process Management
As signal handlers can be called at any time while other code is running, they
should avoid to interfere with memory that is currently accessed.
760
C++ Systems Programming on Linux Process Management
Sending Signals
A process can send a signal to itself or other process by using kill() from the
headers <sys/types.h> and <signal.h>.
• int kill(pid_t pid, int sig)
• pid is the process id of the process that should recieve the signal
• If pid is 0, the signal is sent to all processes in the process group
• If pid is -1, the signal is sent to all processes for which the calling process
has the permission
• Returns 0 on success, -1 on error
• With the signals SIGUSR1 and SIGUSR2 (“user-defined signals”) this can be
used for (limited) communication between processes
761
C++ Systems Programming on Linux Process Management
int fds[2];
pipe(fds);
int readfd = fds[0]; int writefd = fds[1];
write(writefd, "hello", 5);
char buffer[5];
read(readfd, buffer, 5); // buffer now contains "hello"
close(readfd); close(writefd);
762
C++ Systems Programming on Linux Process Management
763
C++ Systems Programming on Linux Error Handling
Error Handling
Most functions use errno from the header <cerrno> for error handling.
• errno is a global variable that contains an error code
• Is set when a function returns an error (e.g. by returning -1)
• All possible values for errno are available as constants:
EINVAL Invalid argument
ENOENT No such file or directory (e.g. in open())
EACCES Permission denied
ENOMEM Not enough memory (e.g. for mmap())
…
• A description of the error can be retrieved with std::strerror() from
<cstring>
764
Miscellaneous
Miscellaneous
765
Miscellaneous Tricks on x86-64
The upper 16 bit of each pointer can be used to store useful information
• Usually called pointer tagging
• Tagged pointers require careful treatment to avoid memory bugs
• If portability is desired, an implementation that works without pointer
tagging has to be provided (e.g. through preprocessor defines)
• Allows us to modify two values (16 bit tag and 48 bit pointer) with a single
atomic instruction
766
Miscellaneous Tricks on x86-64
Guidelines
• Always wrap tagged pointers within a suitable data structure
• Do not expose tagged pointers in raw form
• Store tagged pointers as uintptr_t internally
• Use bit operations to access tag and pointer parts
767
Miscellaneous Tricks on x86-64
768
Miscellaneous Tricks on x86-64
769
Miscellaneous Vectorization
Vectorization
Most modern CPUs contain vector units that can exploit data-level parallelism
• Apply the same operation (e.g. addition) to multiple data elements in a single
instruction
• Can greatly improve the performance of suitable algorithms (e.g. image
processing)
• Not all algorithms are amenable to vectorization
Overview
• Can be used through extensions to the x86 instruction set architecture
• Commonly referred to as single instruction, multiple data (SIMD) instructions
• Can be used in C/C++ code through intrinsic functions
• The Intel Intrinsics Guide provides an excellent documentation
770
Miscellaneous Vectorization
SIMD Extensions
Modern CPUs retain backward compatibility with older instruction set extensions
• The CPU flags exposed in /proc/cpuinfo indicate which extensions are
supported
• We will briefly introduce AVX (avx flag in /proc/cpuinfo)
• AVX should be supported on most reasonably modern x86 CPUs
771
Miscellaneous Vectorization
AVX data types and intrinsics are defined in the <immintrin.h> header
• AVX adds 16 registers which are 256 bits wide each
• Can hold multiple data elements
• Can be used through special opaque data types
Other SIMD extensions follow similar naming conventions for data types
772
Miscellaneous Vectorization
AVX Intrinsics
773
Miscellaneous Vectorization
Constant Values
774
Miscellaneous Vectorization
775
Miscellaneous Vectorization
Arithmetic Operations
776
Miscellaneous Vectorization
Example
Computing the sum of elements in an std::vector
#include <immintrin.h>
#include <vector>
//--------------------------------------------------------------------
float fastSum(const std::vector<float>& vec) {
__m256 vectorSum = _mm256_set1_ps(0);
uint64_t index;
for (index = 0; (index + 8) <= vec.size(); index += 8) {
__m256 data = _mm256_loadu_ps(&vec[index]);
vectorSum = _mm256_add_ps(vectorSum, data);
}
float sum = 0;
float buffer[8];
_mm256_storeu_ps(buffer, vectorSum);
for (unsigned i = 0; i < 8; ++i)
sum += buffer[i];
for (; index < vec.size(); ++index)
sum += vec[index];
return sum;
}
777
Miscellaneous Vectorization
Further Operations
778
Miscellaneous Compile Time Programming
Template Metaprogramming
779
Miscellaneous Compile Time Programming
Type Traits
780
Miscellaneous Compile Time Programming
Using type traits can prevent code duplication. Common example: const and
non-const versions of an iterator.
template <typename T>
class Container {
private:
template <bool isConst>
class Iterator {
public:
using reference = std::conditional_t<isConst, const T&, T&>;
// [...]
};
public:
using iterator = Iterator<false>;
using const_iterator = Iterator<true>;
};
781
Miscellaneous Compile Time Programming
int main() {
auto x = getFibs<10000>();
for (size_t i = 0; i < 10000; ++i) {
std::cout << i << ": " << x[i] << std::endl;
}
return 0;
}
782
Miscellaneous Implementing Concepts
We can use concepts to restrict the types used by templates instead of SFINAE.
#include <concepts>
783
Miscellaneous Implementing Concepts
Concepts can be defined by a single bool (usually computed from type traits).
#include <type_traits>
int x = 0;
foo(x); // OK, returns -1
unsigned y = 0;
foo(y); // Compile time error: constraints not satisfied
784
Miscellaneous Implementing Concepts
785
Miscellaneous Coroutines
Coroutines (1)
Functions have state that has to be maintained across nested function calls
• Values of any local variables
• The instruction at which to resume execution after a function call
• Strict nesting of function calls allows for highly optimized state maintenance
on the stack
• Strict nesting of function calls makes implementing asynchronous operations
cumbersome
786
Miscellaneous Coroutines
Coroutines (2)
Coroutines are functions that can be suspended and resumed (almost) arbitrarily
• Suspending a coroutine transfers execution back to the caller
• Resuming a suspended coroutine continues execution at the point it was
suspended
• The state of a coroutine remains alive across suspensions (e.g. local variables)
Coroutines in C++ are implemented with the help of three new keywords
• co_await <expr>: Suspends the coroutine and returns control to the caller
• co_yield <expr>: Returns a value to the caller and suspends the coroutine
• co_return <expr>: Returns a value to the caller and finishes the coroutine
787
Miscellaneous Coroutines
Coroutines (3)
788
Miscellaneous More Features
Modules (1)
789
Miscellaneous More Features
Modules (2)
Example
greeting.cpp
export module greeting;
import <string>;
main.cpp
import greeting;
import <iostream>;
int main() {
std::cout << getGreeting() << std::endl;
}
790
Miscellaneous More Features
Perfect Forwarding
You can forward parameters keeping the value type with std::forward
#include <iostream>
#include <utility>
void foo(int&& n) {
std::cout << "rvalue overload, n=" << n << "\n";
}
void foo(int& n){
std::cout << "lvalue overload, n=" << n << "\n";
}
template <typename T>
void dispatcher(T&& n){
foo(std::forward<T>(n));
}
dispatcher(1); // rvalue overload, n=1
int i = 2;
dispatcher(i); // lvalue overload, n=2
791
Miscellaneous More Features
Designated Initializers
struct Foo {
int a;
int b;
};
Foo f{ .a = 1, .b = 2 };
792
Miscellaneous More Features
Bit Manipulation
The <bit> header introduces several functions for bit inspection and
manipulation.
• std::bit_cast: Inspect the object representation (instead of using
reinterpret_cast with potential undefined behavior)
• std::endian: Check the endianness of the system
• std::has_single_bit: Check if number is power of two
• std::bit_ceil, std::bit_floor: Find the next/previous power of two
• std::rotl, std::rotr: Rotate bits
• std::countl_zero: Count the number of consecutive zero bits starting
from the most significant bit
• std::popcount: Count the number of one bits
• ...
793
Miscellaneous More Features
struct LargeObject {
char data[1000];
};
std::atomic<std::shared_ptr<LargeObject>> object;
void readThreadSafe() {
auto objectPtr = object.load();
if (objectPtr)
objectPtr->data; /* do something with objectPtr->data */
}
794
Miscellaneous More Features
std::format
795
Miscellaneous More Features
More Features
796
Miscellaneous More Features
797
Miscellaneous More Features
Compiler support for many features is still experimental and very limited.
• std::generator the only part that uses coroutines, is not supported by any
compiler
• Standard Library Modules are currently only supported by MSVC
• Might be not be fully implemented in a while.
• In any case: Use the latest compiler version available to you
798