现代C++
现代c++编程
1 |
|
编译
1 | c++ -std=c++23 -Wall -Werror first.cpp -o first |
运行
1 | ./first |
编译和运行
首先被编译成object code 然后linker会link一些库最后编程可执行文件
- translation unit: input of compiler
- object code is the output of compiler
#include
is done vis the preprocessor (literally included = long compilation time)- compiler uses the declaration to know the signature of object, not resolve external thing
- linker check these symbols
头文件和源文件
- declarations in header files
- definitions in source files
ODR
- all object have at most one definition in any translation unit
- all object that are used have exactly one definitions
Head guards
1 |
或者
1 |
modules (doesn't work yet)
定义module
1 | export module sayHello; |
使用
1 | import sayHello; |
CMake
CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.27) |
cmake_minimum_required
require at least a versionproject
define a project with nameadd_executable
build executable with name and source fileadd_library
define a librarytarget_include_directories(myProgram PUBLIC inc)
where to look for include files for the target “myProgram”target_link_libraries(myProgram PUBLIC myLibrary)
which libraries to add to target “myProgram”
C++ Basic
Types and built-in types
static type safety
a program that violates type safety will not compile, compiler report violation
dynamic type safety
if a program violates type safety it will be detected at run-time
C++ is neither statically nor dynamically type safe!
1 | TYPE variablename{initializer}; |
Integer literal
- decimal:
42
- octal(base 8):
052
- hex:
0x42
1'000'000'000'000ull
careful 0xffff
maybe -1
or
65536
1 | cout << 0xffff << endl; // 65536 |
std::ptrdiff_t
1 | const std::size_t N = 10; |
double (64bit): approx. 15 digits
long double (64-128bit): approx. 19 digits
1.0l
float128_t
Initialization
safe: variableName{}
unsafe: variableName = or variableName()
the unsafe version may do (silent) implicit conversions
local variables are usually not default-initialized, this can lead to undefined behavior when accessing an uninitialized variable
const
const T
a const object is considered immutable and cannot be modified
extern int errorNumber;
declare, no definition
using Cmplx = std::complex;
float sqrt(float);
declares the function sqrt, taking a
float as argument, returning a float, no definition!
1 |
|
Type Alias
1 | typedef int myNewInt; // equivalent to using myNewInt = int |
Scope
1 | int x{0}; // global x |
Lifetime
The lifetime of an object
- starts when its constructor completes
- ends when its destructor starts executing
using an object outside its lifetime leads to undefined behavior!
storage duration: which begins when its memory is allocated and ends when its memory is deallocated, the lifetime of an object never exceeds its storage duration
automatic
allocated at beginning of scope, deallocated when it goes out of scopestatic
: allocated when program starts, lives until end of programdynamic
: allocation and deallocation is handled manually (see later) (usingnew, delete
)thread-local
: allocated when thread starts, deallocated automatically when thread ends; each thread gets its own copy
Auto
1 |
|
Array and Vector
1 |
|
Do not use:
1
2int a[10] = {};
float b[3] = {0.0, 1.1, 2.2};
Not-fixed size array: vector
Loop
1 |
|
lvalue and rvalue
lvalue
- lvalues that refer to the identity of an object, modifiable lvalues can be used on left-hand side of assignment
rvalue
- rvalues that refer to the value of an object, lvalues and rvalues can be used on right-hand side of assignment
Increment
- prefix variants increment/decrement the value of an object and return a reference to the result
- postfix variants create a copy of an object, increment/decrement the value of the original object, and return the unchanged copy
Precedence
operators with higher precedence bind tighter than operators with lower precedence
operators with equal precedence are bound in the direction of their associativity
if, switch
1 | if (auto value{computeValue()}; value < 42) { |
Reference
- LReference
&declarator
- RReference
&&declarator
Refences are special
- there are no references to void
- references are immutable (although the referenced object can be mutable)
- references are not objects, i.e. they do not necessarily occupy storage
1 | int &m{i}; // valid |
- a reference to type T must be initialized to refer to a valid object
there are exceptions:function parameter declarations,function return type declarations,when using extern modifier
1 | int global0{0}; |
Rvalue references cannot (directly) bind to lvalues:
1 | int i{10}; |
Rvalue references can extend the lifetime of temporary objects:
1 | int i{10}; |
Overload resolution (see later) allows to distinguish between lvalues and rvalues:
1 | void foo(int& x); |
references themselves cannot be const, however, the reference type can be const
Dangling references
1 | int& foo() { |
Converting
static cast:
1 | static_cast< new_type > ( expression ) |
- new_type must have same const-ness as the type of expression
Do not use: (const_cast
, reinterpret_cast
,
C-style cast: (new_type) expression)
1 | int sum(int a, int b); |
Function
C++ also allows a trailing return type:
1 | auto foo() -> int; |
Return multiple
1 | std::pair<int, std::string> foo() { |
Structured Binding
1 | std::map<std::string, int> myMap; // map with strings as keys |
you can also bind struct members or std::array entries:
1 | struct myStruct { int a{1}; int b{2}; }; |
specifying the return type for multiple values can be annoying:
1 | auto bar() { |
Parameter passing
- in parameter
1 | void f1(const std::string& s); // OK, pass by const reference |
in-out
parameter1
void update(Record& r); // assume that update writes to r
out
parameterreturn them
Overloading
the compiler automatically resolves the overloads in the current scope and calls the best match
1 | void f(int); // a function called f, taking an int |
Criteria
- exact match. no or only trivial conversions, e.g. T to const T)
- match using promotions (e.g. bool to int, char to int, or float to double)
- match using standard conversions (e.g. int to double, double to int, or int to unsigned int)
- match using user-defined conversions
- match using ellipsis
1 | void print(int); |
Functors
Functions are not objects in C++
- they cannot be passed as parameters
- they cannot have state
However, a type T can be a function object (or functor), if:
- T is an object
- T defines
operator()
1 | struct Adder { |
std::function
is a wrapper for all callable targets
caution: can incur a slight overhead in both performance and memory
1 |
|
other example
1 |
|
Lambda
Lambda expressions are a simplified notation for anonymous function objects
1 | std::find_if(container.begin(), container.end(), |
- the function object created by the lambda expression is called closure
- the closure can hold copies or references of captured variables
[ capture_list ] ( param_list ) -> return_type { body }
capture_list specifies the variables of the environment to be captured in the closure
return_type specifies the return type; it is optional!
if the param_list is empty, it can be omitted
Storing lambda
Lambda expressions can be stored in variables:
1 | auto lambda = [](int a, int b) { return a + b; }; |
- the type of a lambda expression is not defined, no two lambdas have the same type (even if they are identical otherwise)
1 | auto myFunc(bool first) { // ERROR: ambiguous return type |
Capture
- all capture: by copy
=
, by reference&
1 | void f() { |
Exception
1 |
|
- when an exception is thrown, C++ performs stack unwinding
ensures proper clean up of objects with automatic storage duration
1 |
|
No except
1 | int myFunction() noexcept; // may not throw an exception |
- exceptions should be used rarely, main use case: establishing class invariants
- exceptions should not be used for control flow!
- some functions must not throw exceptions,
- destructors,move constructors and assignment operators
expected
std::expected<T,E>
stores either a value of type T
or an error of type E
has_value()
checks if the expected value is therevalue()
or*
accesses the expected valueerror()
returns the unexpected valuemonadic operations are also supported, such as
and_then()
oror_else()
performance guarantee: no dynamic memory allocation takes place
1 | enum class parse_error { invalid_input }; |
std::expected
is well suited for more regular errors, such as errors in parsing- exception handling is well suited for the rare failures you cannot do much about at the called function (e.g. out of memory)
STL
optional
encapsulates a value that might or might not exist
1 | std::optional<std::string> mightFail(int arg) { |
check value
1 | mightFail(3).has_value(); // true |
default
1 | mightFail(42).value_or("default"); // "default" |
reset
1 | auto opt6 = mightFail(6); |
monadic operations
and_then
: if optional has value, returns result of provided function applied to the value, else empty optionalor_else
: if optional has value, return optional itself, otherwise result of provided functiontransform
if optional has value, return optional with transformed value, else empty optional
1 | std::optional<int> foo() { /* ... */ } |
Pair
1 | std::pair<int, double> p1(123, 4.56); |
structure binding
1 | auto t = std::make_tuple(123, 4.56); |
variant
to store different alternative types in one object
1 | std::variant<int, float> v; |
apply different
1 | struct SampleVisitor { |
without struct, with overload pattern
1 | template<class... Ts> struct overload : Ts... { using Ts::operator()...; }; |
String
std::string
encapsulates character sequences
- manages its own memory (RAII)
- guaranteed contiguous memory storage
- since C++20, it has constexpr constructors
alias for std::basic_string<char>
1 | std::string s{"Hello World!"); |
at() is bounds checked, [] is not, both return a reference
1 | s.at(4) = 'x'; |
append()
and +=: appends another string or character+
concatenates stringsfind
returns offset of the first occurrence of the substring, or the constantstd::string::npos
if not found
string view
provides a read-only view on already existing strings
substr()
creates another string view in \(O(1)\)
1 | std::string s{"garbage garbage garbage interesting garbage"}; |
in fuction
1 | // useful for function calls |
Container
Vector
emplace bace
1 | struct ExpensiveToCopy { |
reserve
1 | std::vector<int> vec; |
fit
1 | std::vector<int> vec; |
Other
std::array
for compile-time fixed-size arraysstd::deque
is a double-ended queuestd::list
is a doubly-linked liststd::forward_list
is a singly-linked liststd::queue
provides the typical queue interface, wrapping one of the sequence containers with appropriate functionsstd::stack
provides the typical stack interface, wrapping one of the sequence containers with appropriate functions (deque, vector, list)std::priority_queue
provides a typical priority interface, wrapping one of the sequence containers with random access (vector, deque)
unordered_map
unordered maps are associative containers of key-value pairs, keys are required to be unique
is internally a hash table
1 | std::unordered_map<std::string, double> nameToGrade {{"maier", 1.3}, {"huber", 2.7}, {"lasser", 5.0}}; |
search
1 | auto search = nameToGrade.find("maier"); |
check exist
1 | nameToGrade.count("blafasel"); // == 0 |
emplace
1 | auto p = std::make_pair("mueller", 2.0); |
remove
1 | auto search = nameToGrade.find("lasser"); |
map
1 | std::map<int, int> xToY {{1, 1}, {3, 9}, {7, 49}}; |
unordered set
- elements must not be modified! If an element’s hash changes, the container might become corrupted
Thread safety
- two different containers do not share state, i.e. all member functions can be called concurrently by different threads
- all const member functions can be called concurrently.
- iterator operations that only read (e.g. incrementing or dereferencing an iterator) can be run concurrently with reads of other iterators and const member functions
- different elements of the same container can be modified concurrently
rule of thumb: simultaneous reads on the same container are always OK, simultaneous read/writes on different containers are also OK. Everything else requires synchronization!
Iterator
different element access method for each container, i.e. container types not easily exchangeable in code
1 | *end; // undefined behavior! |
- range-for loop can also be used to iterate over a container; it requires the range expression to have a begin() and end() iterator
1 | for (auto elem : vec) |
Dynamic
1 | for (it = vec.begin(); it != vec.end(); ++it) { |
- InputIterator and OutputIterator are the most basic iterators
- equality comparison: checks if two iterators point to the same position
- a dereferenced InputIterator can only be read
- a dereferenced OutputIterator can only be written to
- ForwardIterator combines InputIterator and OutputIterator
- BidirectionalIterator generalizes ForwardIterator
- RandomAccessIterator generalizes BidirectionalIterator
- additionally supports random access with
operator[]
- additionally supports random access with
- ContiguousIterator guarantees that elements are stored in memory
contiguously
- extends RandomAccessIterator
Ranges
1 |
|
- a range is anything that can be iterated over
- i.e. it has to provide a begin() iterator and an end() sentinel
- a view is something that you apply to a range and which performs
some operation
- a view does not own data
- time complexity to copy/move/assign is constant
- can be composed using the pipe symbol
|
- views are lazily evaluated
1 |
|
Algorithm
- sort
1 |
|
std::stable_sort
- partially sorting a range:
std::partial_sort
- check if a range is sorted:
std::is_sorted
,std::is_sorted_until
Searching
unsorted:
find the first elements satisfying some criteria:
std::find
std::find_if
std::find_if_not
search for a range
std::search
count match
std::count
std::count_if
1 |
|
sorted
std::binary_search
lookup an element in sorted range [first, last), only checks for containment, therefore return type is bool
std::lower_bound
returns iterator pointing to the first element >= the search value
std::upper_bound
returns iterator pointing to first element > the search value
std::equal_range
returns pair of iterators (begin and end of range)
Permutation
initialize a dense range of elements:
std::iota
iterate over permutations in lexicographical order
std::next_permutation
std::prev_permutation
iota fills the range [first, last) with increasing values, starting at value
1 |
|
More feature
std::min
andstd::max
over a range instead of two elementsstd::merge
andstd::in_place_merge
for merging of sorted ranges- multiple set operations (intersection, union, difference, ...)
- heap functionality using
std::make_heap
,std::push_heap
,std::pop_heap
- sampling of elements using
std::sample
- swapping elements using
std::swap
- range modifications:
std::copy
to copy elements to a new locationstd::rotate
to rotate a rangestd::shuffle
to randomly reorder elements
Stream and IO
std::istream
is the base class for input operations,
alias for std::basic_istream<char>
std::ostream
is the base class for output
operations,
good(), fail(), bad()
: checks if the stream is in a specific error stateeof()
: checks if the stream has reached end-of-fileoperator bool(): returns true if stream has no errors
input stream
get()
: reads single or multiple characters until a
delimiter is found
1 | // defined by the standard library: |
output stream
put()
: writes a single character
1 | // defined by the standard library: |
stringstream
std::stringstream
can be used when input and output
should be read and written from a std::string
1 | std::stringstream stream("1 2 3"); |
filestream
1 | std::ifstream input("input_file"); |
Caveats of streams
1 | std::cout << std::format("{} {}: {}!\n", "Hello", "World", 2023); |
since c++23
1 | std::println("{} {}: {}!", "Hello", "World", 2023); |
Object-oriented Programming
class_keyword is either class or struct
1 | struct Foo { |
Member function
1 | struct Foo { |
out line
1 | struct Foo { |
Access
1 | struct C { |
the nested type has full access to the parent (friend, see later)
class
- class have default access private
- struct have default access public
1 | class Foo { |
Friend
- you can put friend declarations in the class body, granting the friend access to private and protected members of the class
friend function_declaration ;
declaring a non-member function as friend of the class (with an implementation elsewhere)friend function_definition ;
defines a non-member (!) function and declares it as friend of the classfriend class_specifier ;
declares another class as friend of the class
1 | class A { |
classes can be forward declared:
class_keyword name ;
this declares an incomplete type that will be defined later in the scope
const correctness
1 | class A { |
overload
1 | struct Foo { |
cfoo.getA() == 2, cfoo.getB() == 2
, whilecfoo.getC()
gives an error
Ref qualifier
1 | struct Bar { |
- if you use ref-qualifiers for a member function, all of its overloads need to have ref-qualifiers (cannot be mixed with normal overloads)
Static class member
1 | struct Date { |
Constroctor and deconstructor
constructors consist of two parts:
- an initializer list (which is executed first)
- a regular function body (executed second)
1 | struct A { |
- constructors with exactly one argument are special: they are used for implicit and explicit conversions
- if you do not want implicit conversion, mark the constructor as explicit
1 | struct Foo { |
Copy constructorr
constructors of a class C that have a single argument of type
C&
or const C&
(preferred) are called
copy constructors
1 | struct Foo { |
deconstructor
1 | void f() { |
Operator overloading
return_type operator op (arguments)
1 | struct Int { |
binary
1 | struct Int { |
Increment
1 | struct Int { |
assignment
1 | struct Int { |
conversions
1 | struct Int { |
explicit
1 | struct Float { |
Argument-dependent lookup
1 | namespace A { |
Enum class
- by default, the underlying type is an int
- by default, enumerator values are assigned increasing from 0
1 | enum class TrafficLightColor { |
RAII - Resource Acquisition is Initialization
bind the lifetime of a resource (e.g. memory, sockets, files, mutexes, database connections) to the lifetime of an object
Implementation:
- 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 (see later)
1 | void writeMessage(std::string message) { |
Copy semantics
- construction and assignment of classes employs copy semantics in
most cases
- by default, a shallow copy is created
Copy constructor
the copy constructor is invoked whenever an object is initialized from an object of the same type, its syntax is
1 | class_name ( const class_name& ) |
the copy constructor is invoked for
- copy initialization:
T a = b;
- direct initialization:
T a{b};
- function argument passing:
f(a);
wherevoid f(T t)
- function return:
return a;
inside a functionT f();
(if T has no move constructor, see later)
1 | class A { |
the compiler will implicitly declare a copy constructor if no user-defined copy constructor is provided
- the implicitly declared copy constructor will be a public member of the classthe implicitly declared copy constructor will be a public member of the class
- the implicitly declared copy constructor may or may not be defined!
the implicitly declared copy constructor is defined as = delete if one of the following is true:
- the class has non-static data members that cannot be copy-constructed
- the class has a base class which cannot be copy-constructed
- the class has a base class with a deleted or inaccessible destructor
- the class has a user-defined move constructor or move assignment operator
Copy assignment
the compiler will implicitly declare a copy assignment operator if no user-defined copy assignment operator is provided
the implicitly declared copy assignment operator is defined as = delete if one of the following is true:
- the class has non-static data members that cannot be copy-assigned
- the class has a base class which cannot be copy-assigned
- the class has a non-static data member of reference type
- the class has a user-defined move constructor or move assignment operator
if it is not deleted, the compiler defines the implicitly-declared copy constructor
- only if it is actually used
- it will perform a full member-wise copy of the object’s bases and members in their initialization order
- uses direct initialization
1 | struct A { |
often, a class should not be copyable anyway if the implicitly generated versions do not make sense
guidline:
- you should provide either provide both copy constructor and copy assignment, or neither of them
- the copy assignment operator should usually include a check to detect self-assignment
- if possible, resources should be reused; if resources cannot be reused, they have to be cleaned up properly
1 | class A { |
The Rule of Three
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
Move Semantics
move constructor
- the move constructor is invoked when an object is initialized from an rvalue reference of the same type
class_name ( class_name&& ) noexcept
- the noexcept keyword is optional, but should be added to indicate that the move constructor never throws an exception
the function std::move
from #include
can be
used to convert an lvalue to an rvalue reference
1 | struct A { |
the move constructor for class type T and objects a,b is invoked for
- direct initialization:
T a{std::move(b)};
- copy initialization:
T a = std::move(b);
- passing arguments to a function
void f(T t);
viaf(std::move(a))
- returning from a function
T f();
via return a;
Move assignment
the move assignment is invoked when an object appears on the left-hand side of an assignment with an rvalue reference on the right-hand side
class_name& operator=( class_name&& ) noexcept
the noexcept keyword is optional, but should be added to indicate that the move assignment never throws an exception
1 | struct A { |
the compiler will implicitly declare a public move constructor, if all of 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
the implicitly declared move constructor is defined as = delete if
one of the following is true:
- the class has non-static data members that cannot be moved
- the class has a base class which cannot be moved
- the class has a base class with a deleted or inaccessible destructor
the compiler will implicitly declare a public move assignment operator if all of 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 constructors
- there are no user-declared destructors
the implicitly declared copy assignment operator is defined as = delete if one of the following is true:
- the class has non-static data members that cannot be moved
- the class has non-static data members of reference type
- the class has a base class which cannot be moved
- the class has a base class with a deleted or inaccessible destructor
if it is not deleted, the compiler defines the implicitly-declared move constructor
- only if it is actually used
- it will perform a full member-wise move of the object’s bases and members in their initialization order
- uses direct initialization
if it is not deleted, the compiler defines the implicitly-declared move assignment operator
- only if it is actually used
- it will perform a full member-wise move assignment of the object’s bases and members in their initialization order
- uses built-in assignment for scalar types and move assignment for class types
1 | struct A { |
custom move constructors/assignment operators are often necessary
guideline:
- you should either provide both move constructor and move assignment, or neither of them
- the move assignment operator should usually include a check to detect self-assignment
- the move operations typically do not allocate new resources, but steal the resources from the argument
- the move operations should leave the argument in a valid (but indeterminate) state
- any previously held resources must be cleaned up properly!
1 | class A { |
The Rule of Five
- if a class wants move semantics, it has to define all five special member functions
- if a class wants only move semantics, it still has to define all
five special member functions, but define the copy operations
as
= delete
The Rule of Zero
- classes not dealing with ownership (e.g. of resources) should not have custom destructors, copy/move constructors or copy/move assignment operators
- classes that do deal with ownership should do so exclusively (and follow the Rule of Five)
Copy and swap
if copy assignment cannot benefit from resource reuse, the copy and swap idiom may be useful:
- the class only
defines
class_name& operator=( class_name )
copy-and-swap assignment operator
it acts as both copy and move assignment operator, depending on the value category of the argument
1 | class A { |
Copy elision(lvalue, rvalue)
glvalues identify objects
xvalues identify an object whose resources can be reused
1
2int x;
f(std::move(x))prvalues compute the value of an operand or initialize an object
Compilers have to omit copy/move construction of class objects under certain circumstances:
- in a return statement, when the operand is a prvalue of the same class type as the function return type:
1 | T f() { |
Ownership semantics
a resource should be owned (encapsulated) by exactly one object at all times, ownership can only be transferred explicitly by moving the respective object
- always use ownership semantics when managing resources in C++ (such as memory, file handles, sockets etc.)!
Smart pointer
unique pointer
std::unique_ptr
is a smart pointer implementing ownership semantics for arbitrary pointersassumes unique ownership of another object through a pointer
automatically disposes of that object when
std::unique_ptr
goes out of scopea
std::unique_ptr
can be used (almost) exactly like a raw pointer, but it can only be moved, not copieda
std::unique_ptr
may be empty
ALWAYS use std::unique_ptr
over raw pointers!
creation:
1 | std::make_unique<type>(arg0, ..., argN) |
- the
get()
member function returns the raw pointer release()
member function returns the raw pointer and releases ownership(very rarely required)
1 | struct A{ |
array example
1 | std::unique_ptr<int[]> foo(unsigned size) { |
Advanced example
1 |
|
shared pointer
std::shared_ptr
is a smart pointer implementing shared
ownership
- a resource can be simultaneously shared with several owners
- the resource will only be released once the last owner releases it
- multiple
std::shared_ptr
objects can own the same raw pointer, implemented through reference counting - a
std::shared_ptr
can be copied and moved
use std::make_shared
for creation
to break cycles, you can use std::weak_ptr
1 |
|
guidline:
std::unique_ptr
should almost always be passed by value- raw pointers represent resourcesraw pointers represent resources
- references grant temporary access to an object without assuming ownership
1 | struct A { }; |
DO NOT DO:
1 | int* i; |
OOP2
Derived Class
any class type (struct or class) can be derived from one or several base classes
1 | class class_name : base_specifier_list { |
base specifier list
1 | access_specifier virtual_specifier base_class_name |
example
1 | class Base { |
constructor
1 | struct Base { |
deconstructor
1 | struct Base0 { |
qualified look up
1 | struct A { |
inheritance mode
- public means the public base class members are public, and the protected members are protected
- protected means the public and protected base class members are only accessible for class members / friends of the derived class and its derived classes
- private means the public and protected base class members are only accessible for class members / friends of the derived class
Virtual function and polymorphy
inheritance in C++ is by default non-polymorphic, the type of the object determines which member is referred to
1 | struct Base { |
virtual function
non-static member functions can be marked virtual, a class with at least one virtual function is polymorphic
- only the function in the base class needs to be marked virtual, the
overriding functions in derived classes do not need to be (and should
not be) marked
virtual
- instead they are marked
override
1 | struct Base { |
with override
1 | struct Base { |
to prevent overriding a function it can be marked as final:
1 | struct Base { |
Virtual deconstructor
- derived objects can be deleted through a pointer to the base class, this leads to undefined behavior unless the destructor in the base class is virtual
- hence, the destructor in a base class should either be protected and non-virtual or public and virtual (this should be the default)
1 | struct Base { |
Slicing
inheritance hierarchies need to be handled via references (or pointers)!
1 | struct A { |
another example
1 | struct A { |
in most of the cases you should then = delete all copy/move operations to prevent slicing
1 | class BaseOfFive { |
to enable copying of polymorphic objects, you can define a factory method
1 | struct A { |
convert
to convert references (or pointers) in an inheritance hierarchy in a safe manner, you can use
1 | dynamic_cast< new_type > ( expression ) |
example
1 | struct A { |
- polymorphism does not come for free, it has overhead! Implementation:vtable
- run-time cost,each virtual function call has to: follow the pointer to the vtable, follow the pointer to the actual function
- memory cost,polymorphic objects have larger size, as they have to store a pointer to the vtable
Pure virtual function
a virtual function can be marked as a pure virtual function by
adding= 0
at the end of the declarator/specifiers
abstract classes are special:
they cannot be instantiated
but they can be used as a base class (defining an interface)
references (and pointers) to abstract classes can be declared
caution: calling a pure virtual function in the constructor or destructor of an abstract class is undefined behavior!
The destructor can be marked as pure virtual
- useful when class needs to be abstract, but has no suitable functions that could be declared pure virtual
1 | struct Base { |
shared from this
when you want to create std::shared_ptr of the class you are currently in, i.e. from this, you need to be careful
1 | struct S : std::enable_shared_from_this<S> { |
caveat: this only works correctly if shared_from_this is called on an object that is already owned by a std::shared_ptr!
Concept of generic programming
template declaration syntax:
1 | template < parameter_list > declaration |
parameter_list is a comma-separated list of template parameters
- type template parameters
- non-type template parameters
- template template parameters
1 | template <typename T, unsigned N> |
template of template
1 | template <template <class, unsigned> class ArrayType> |
template parameters with default values cannot be followed by template parameters without default values
template arguments for template template arguments must name a class template or template alias
1 |
|
alias template
1 | namespace something::extremely::nested { |
template instantiation
explicitly force instantiation of a template specialization
1 | template class template_name < argument_list >; |
example
1 | template <typename T> |
Implicit template instantiation:
1 | template <typename T> |
Out of line definition
1 | template <typename T> |
Disambiguator
if such a name should be considered as a type, the typename disambiguator has to be used
1 | struct A { |
if such a name should be considered as a template name, the template disambiguator has to be used
1 | template <typename T> |
reference collapsing
1 | template <typename T> |
- rvalue reference to rvalue reference collapses to rvalue reference
- any other combination forms an lvalue reference
Template specialization
general
1 | template <> declaration |
example
1 | template <typename T> |
Partial specialization
1 | template <typename C, typename T> |
Template argument deduction
1 | template <typename T> |
sometimes ambigouus
1 | template <typename T> |
class deduction
1 |
|
deduction guide
1 | using namespace std::string_literals; |
stl
1 | // let std::vector<> deduce element type from initializing iterators: |
auto
auto does not require any modifiers to work, however, it can make code hard to understand and error prone, so all known modifiers should always be added to auto
1 | const int* foo(); // using raw pointers for illustrative purposes only |
auto is not deduced to a reference type,
this might incur unwanted copies, so always add all known modifiers to auto
1 | struct A { |
template lambda
1 | // generic lambda: |
in fact, since C++20, also ordinary functions can take auto parameters as well:
1 | void f1(auto x) { /* ... */ } |
this automatically translates into a function template with an invented template parameter for each auto parameter
Parameter pack
parameter packs are template parameters that accept zero or more arguments
- non-type:
type ... Args
- type:
typename|class ... Args
- template:
template < param_list > typename|class ... Args
1 | // a variadic class template with one parameter pack |
example
1 |
|
fold expression
fold expressions can be used to reduce a parameter pack over a binary
operatorop
- variant 1 (unary left): ( ... op pack )
- variant 2 (unary right): ( pack op ... )
- variant 3 (binary left): ( init op ... op pack )
- variant 4 (binary right): ( pack op ... op init )
- pack must be an expression containing an unexpanded parameter pack
- init must be an expression not containing a parameter pack
semantics:
- \((\ldots \circ E)\) becomes \(\left(\left(E_1 \circ E_2\right) \circ \ldots\right) \circ E_n\)
- \((E \circ \ldots)\) becomes \(E_1 \circ\left(\ldots\left(E_{n-1} \circ E_n\right)\right)\)
- \((I \circ \ldots \circ E)\) becomes \(\left(\left(\left(I \circ E_1\right) \circ E_2\right) \circ \ldots\right) \circ E_n\)
- \((E \circ \ldots \circ I)\) becomes \(E_1 \circ\left(\ldots\left(E_{n-1} \circ\left(E_n \circ I\right)\right)\right)\)
unary fold:
1 | template <typename R, typename... Args> |
binary fold:
1 | template <typename... T> |
previous print
1 |
|
Compile-time programming
- const: do not modify this object in this scope (variables, member functions)
- constexpr: something that can be evaluated at compile-time
the keyword constexpr
declares a variable or function to
be able to be evaluated at compile-time
constexpr variables must be
- of literal type (e.g. scalar, reference, or user-defined type with constexpr constructor/destructor)
- immediately initialized with a constant expression
constexpr functions must
- have literal return types and arguments
- not contain things like goto or variable definitions of non-literal types or static/thread storage duration
1 | int f(int x) { return x * x; } |
function
constexpr functions are like regular functions, except they cannot have side effects
- since C++20, exceptions and exception handling may be used
to make your user-defined type (class) a literal type, it needs
- a constexpr destructor (or trivial before C++20)
- to have at least one constexpr constructor (that is not copy/move), or it could be an aggregate type
if you want the member function to be const, you have to additionally declare it as such
1 | struct A { |
constexpr
functions can still be called at run-time
if you want guaranteed execution at compile-time, C++20 introduced the consteval keyword to enable immediate functions
1 | consteval int sqr(int n) { |
example:
1 | int sqrRunTime(int n) { return n * n; } |
if
with if constexpr we can evaluate conditions at compile-time
1 |
|
for return
1 |
|
C++23 introduced another if statement: if consteval
syntax:if consteval { /* A */ } else { /* B */ }
- no condition
- mandatory braces
- else branch is optional
1 | consteval int f(int i) { return i; } |
by default, lambdas are implicitly constexpr if possible
1 | auto squared = [] (auto x) { return x * x; } ; |
to ensure that a lambda is constexpr, you can declare it as constexpr after the argument list, before the optional trailing return type
1 | auto squared2 = [] (auto x) constexpr { return x * x; }; |
Template meta programming3
1 | template <unsigned N> |
using function
1 |
|
using if constexpr
1 | template <unsigned N> |
static assert
checks assertions at compile-time
1 | static_assert ( bool_constexpr, message ) |
Type traits
make your own type traits
1 | template <typename T> |
from std:
std::is_integral, std::is_function, is_lvalue_reference
is_polymorphic, is_signed, is_aggregate
is_move_constructible, has_virtual_destructor
is_same, is_base_of, is_invocable
remove_const, make_signed, common_type
enable if
check if T is calcable
1 | template <typename T, |
or
1 | template <typename T, |
check if all same type
1 | // check if passed types are homogeneous |
using c++20 concept
1 |
|
Expression template
- instead of evaluating each intermediate step into a temporary, build an expression tree
- only when assigning the result, perform the actual computation by traversing the expression tree
- also known as lazy evaluation
1 | using add = /* ... */; |
then
1 | // all temporaries in one step: |
implementation
1 | template <typename op, typename... operands> |
operator
1 | template <typename LHS, typename RHS> |
assginment
1 | class vector { |
subscript
1 | template <typename operand> |
type trais
1 | template <typename> |
more careful
1 | template <typename LHS, typename RHS> |
Curiously Recurring Template Pattern (CRTP)
1 | template <typename T> |
do something
1 | template <typename T> |
while CRTP works, it is tricky to use and can lead to insidious errors
- for example: since there are no virtual functions, there is no overriding
- instead, methods of the same name in the derived class will simply hide base class methods!
anothor example
1 | template <typename T> |
in cpp
1 | template <typename T> |
polymorphic cloneing
1 | class Cloneable { |
Concept of parallel
unsynchronized accesses (data races), deadlocks, and other potential issues when using threads are undefined behavior!
1 | void foo(int a, int b); |
the member function join() is used to wait for a thread to finish
join() must be called exactly once for each thread!
if a thread was not joined and the std::thread destructor is called, the program is terminated
alternatively, you can call detach() to have the thread execute independently from its creator
C++20 adds std::jthread, a joining thread
1 | std::jthread t1([] { std::cout << "Hi!\n"; }); |
additionally it supports interruption signaling via std::stop_token
std::thread and std::jthread are not copyable, but they can be moved
1 | std::thread t1([] { std::cout << "Hi!\n"; }); |
can be used in container
1 | std::vector<std::thread> threadPool; |
std::this_thread::sleep_for()
stop the current thread for a given amount of timestd::this_thread::sleep_until()
stop the current thread until a given point in timestd::this_thread::yield()
let the operating system schedule another threadstd::this_thread::get_id()
get the (operating system specific) id of the current thread
1 | using namespace std::literals; |
Mutex
std::mutex
for mutual exclusionstd::recursive_mutex
for recursive mutual exclusionstd::shared_mutex
for mutual exclusion with shared locks
locking
std::unique_lock
forstd::mutex
std::shared_lock
forstd::shared_mutex
std::scoped_loc
k for deadlock free locking of several mutexes
std::mutex
is an exclusive synchronization primitive
lock()
locks the mutex, blocking all other threadsunlock()
will unlock the mutextry_lock()
tries to lock the mutex and returns true if successful
1 | std::mutex printMutex; |
recursive mutexes are regular mutexes that additionally allow a thread that currently holds the mutex to lock it again
1 | std::recursive_mutex m; |
shared mutex
1 | int value{0}; |
std::unique_lock
wraps mutexes using RAII
1 | std::mutex m; |
when using multiple mutexes, deadlocks can occur
consistent ordering avoids this
use std::scoped_lock
when locking multiple mutexes in
consistent order, to guarantee no deadlocks
1 | std::mutex m1, m2, m3; |
Condition
condition variables can be used to synchronize threads by waiting until an (arbitrary) condition becomes true
wait()
: takes a reference to a std::unique_lock that must be locked by the caller as an argument, unlocks the mutex and waits for the condition variablenotify_one()
: notify a single waiting thread, mutex does not need to be held by the callernotify_all()
: notify all waiting threads, mutex does not need to be held by the caller
1 | std::mutex m; |
and
1 | void workerThread() { |
atomic
threads can also be synchronized with atomic operations that are usually more efficient than mutexes
1 | int a{10}; |
std::atomic<T>
is a class that represents an
atomic version of the type T
- T load(): loads the value
- void store(T desired): stores desired in the object
- T exchange(T desired): stores desired in the object and returns the old value
if T is an integral type, the following operations also exist:
T fetch_add(T arg)
: adds arg to the value and returns the old valueT operator+=(T arg)
: add arg to the value and returns the new value
1 | std::atomic<int> i{0}; |
copy and ref
1 | int val{5}; |
std::atomic_flag provides an atomic boolean with a special interface,it is guaranteed to be lock-free
- true by calling
test_and_set()
- false by calling
clear()
test()
returns the current value
STL algo
1 | std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9}; |
C++20 introduced many more parallel programming features
- coroutines,counting semaphores for shared resources,latches and barriers to coordinate threads
C++20 concept
concepts allow
- specifying requirements for template parameters
- overloading of functions
- specialization of class templates
1 | template <typename T> |
with a trailing requires clause:
1 | template <typename T> |
using a constrained template parameter:
1 | template <std::integral T> |
using an abbreviated function template:
1 | auto gcd4(std::integral auto a, std::integral auto b) { /* ... */ } |
basically anywhere you can put auto, you can put
<concept>auto
instead
1 | template <typename Iter, typename Val> |
predefined concept
- same_as, derived_from, convertible_to, assignable_from, swappable
- integral, signed_integral, unsigned_integral, floating_point
- destructible, constructible_from, default_constructible, move_constructible, copy_constructible
- equality_comparable, totally_ordered
- movable, copyable, semi_regular, regular
own concept
1 | template <template-parameter-list> |
example
1 |
|
requirer expression
1 | template <typename T> |
type requirement
1 | template <typename T> |
compound req
1 | template <typename T> |
use if
1 | struct First { |
Short Exercise
write a function translate parameters to a vector, or set
use functional programming get primes less than
200
how to init Vector with
Vector<double> v1{1,3,4,5};
use variant to model a mixture of plus lambda and multiply lambda. evaluate arithmetic expression
use varidic template implement addition of arbitary vector
OOP
- implement Polymorphic cloning demo
- Use shared pointer implement segment tree
- implement difference array and copy, extend normal array
- implement Graph, Tree, structure. find depth in complie time,given tree
- use CRPT , implment polymorhpy
Concept
- A gcd function, require the arguments to be equal and integral, 2 methods,Type traits
Parallel programming
- compute matrix multiplication, use worker and await
- A txt file contains a password
<10000
use a thread to set a passswd randomly and use multithread clients to crack the password
Compile time
- use expression template to implement vector and scalar multiplication