Repository to note important points on C++ talks
- CPP-Talks
- 2020
- 2019
- 2018
- 2017
- 2016
- pair
- tuple
- optional
- variant
- C++ tools that trade performance for usability (e.g. std::shared_ptr, std::function)
- Throwing exceptions on likely code path
- Dynamic polymorphism
- Multiple inheritance
- RTTI (Run Time Type Information): using dynamic_cast, typeid operators & typeinfo class
- Dynamic memory allocations
- Dynamic memory is utilized using
malloc
&free
in C ,new
&delete
in C++.struct A {}; A* ptr = (A*)malloc(sizeof(A)); // A pointer to a heap allocated A free(ptr); // deallocates memory ptr = (cast-type*) malloc(byte-size) ptr = (cast-type*)calloc(n, element-size); ptr = realloc(ptr, newSize); // where ptr is reallocated with new size 'newSize'. Maintains already allocated values
A* ptr = new A(); // calls malloc & constructor delete ptr; // calls destructor, and free A* ptrA = new A[10]; // Creates 10 As delete [] ptrA; // free them struct B { B(int i) {} }; B* pt = new B(10); new (pt) B(100); // placement new. Use existing memory to create object B* ptr = new B[3]{1, 2, 3}; // constructs 3 Bs with different args
- Allocators & allocation process. Allocator is an implementation of malloc and free functions to allow the programme to allocate and deallocate memory.
- Allocator internally asks for large chunck of memory from OS and serves the program with smaller chunchks when malloc is called.
- In a long running programs continuous allocation and deallocation causes memory to be fragmented.
- Tackling memory fragmentation:
- restart the program occatinally
- preallocate all the needed memory at the program beginnig. (Object pools)
- cache memory chunks
- use special memory allocators that promise low fragmentaion
- A program's performance will also depend on the way the memory is accessed. More specifically,
- How the data is laid out in memory.
- What is the access pattern of the data.
- Simple abstraction of malloc/free doesn't take the into account the underlying hardware. The highest performance can be gained by breaking the allocator abstraction. ie. if the algorithm knows how malloc/free works in order to allocate memory optimally.
-
Memory speed is a bottle neck on mordern systems. It can take around 300 CPU cycles to load data from main memory to CPU register.
-
On CPU memory called Cache memory is used to mitigate this.
-
Cache is divided in to cache lines. (typically 64bytes in modern systems)
-
If data is oriented in a manner that they fit to the same cache line the access is optimised.
-
Cache memory types:
- Insturction cahce
- Data cache
- TLB cache - used to translate virtual memory to physical address
-
Overhead of polymorpism
- When it comes to polymorphism, the address of a non-virtual function is known at compile-time and the compiler can emit the call directly. The address of a virtual function is not known at compile-time and the compiler needs to figure it out during runtime.
- For each class that has virtual functions, there is a virtual table (vtable) that contains the address of each of its virtual functions. Additionally, each instance of the type contains a pointer to this list (vtable*). This is a hidden instance member.
* Array of pointers to objects
* This can be prevented by using vector of objects instead of vector of object pointers. But it doesn't play nice with object oriented design. Specially polymorphism. * std::variant is also a better apporach in this.
- Key words
void f(){ throw std::runtime_error(); } try { f(); } catch(std::exception const& ex) { }
- Stack unwinding
- Objects on the stack are destroyed in the reverse order they were created.
- Unhandle exceptions result in std::terminate getting called.
- For errors that are expected to happen rarely.
- For exceptional cases that cannot be handled locally. (I/O errors)
- File not found.
- Map key not found.
- For operators and constructors (i.e. When no other mechanism works)
- For errors that are expected to happen frequently.
- For functions that are excpeted to fail. (to_int(string))
- If you have to guarantee certain response times.
- For things that should never happen. (Assert)
- Dereference nullptr
- out of range access
- Use after free
- Build on the std::exception Hierarchy
- Throw by Rvalue
throw std::exception();
- Catch by reference
- Exception unsafe
- No guarantees.
- Basic safety guarantee
- Invariants are preserved.
- Resources are not leaked.
- Internal state might be changed.
- Strong Exception Safety Guarantee
- Basic safety guarantees.
- No state change. (commit or rollback)
- Not always possible.
- No-Throw Guarantee
- Operation cannot fail.
- Expressed in the code with
noexcept
- Destructors
- Move operations
- Swap Operations
inline auto& SingletonFoo::getInstance() {
static SingletonFoo instance;return instance;
}
- The first thread to arrive will start initializing the static instance.Any more that arrive will block and wait until the first thread either succeeds (unblocking them all) or fails with an exception (unblocking one of them)
class Logger {
std::once_flag once_;
std::optional<NetworkConnection> conn_;
NetworkConnection& getConn() {
std::call_once(once_, []() {
conn_ = NetworkConnection(defaultHost);
});
return *conn_; }};
- Here, the first access to conn_ is protected by a once_flag.This mimics how C++ does static initialization, but for a non-static. Each Logger has its own conn_, protected by its own once_.
class ThreadSafeConfig
{
std::map<std::string, int> settings_;
mutable std::shared_mutex rw_;
void set(const std::string &name, int value)
{
std::unique_lock<std::shared_mutex> lk(rw_);
settings_.insert_or_assign(name, value);
}
int get(const std::string &name) const
{
std::shared_lock<std::shared_mutex> lk(rw_);
return settings_.at(name);
}
};
- unique_lock calls rw_.lock() in its constructor and rw_.unlock() in its destructor.shared_lock calls rw_.lock_shared()in its constructor and rw_.unlock_shared()in its destructor.
std::latch myLatch(2);
std::thread threadB = std::thread([&]() {
myLatch.arrive_and_wait();
printf("Hello from B\n");
});
printf("Hello from A\n");
myLatch.arrive_and_wait();
threadB.join();
printf("Hello again from A\n");
std::barrier b(2, [] { puts("Green flag, go!"); });
std::thread threadB = std::thread([&]() {
printf("B is setting up\n");
b.arrive_and_wait();
printf("B is running\n");
});
printf("A is setting up\n");
b.arrive_and_wait();
printf("A is running\n");
threadB.join();
- Single Responsibility
- Open Close
- Liscove's Substitution
- Interface Segregation
- Dependency Inversion
- Orthogonality: Software Components (functions, classes, modules) should be self contained, with a single, well defined purpose. One should be able to change without worrying about the others.
- Cohesion measures the strength of association inside a module. A highly cohesive module is a collection of statements and data that should be treated as a whole.
BAD | Good |
---|---|
![]() |
![]() |
- Guideline: Prefer cohesive software entities. Everything that does not strictly belong together, should be separated
- Software should be open for extension but closed for modification.
- Creating an object hierarchy based on types and switching between them violates open close principle. Creating an object heirarchy based on operations (virtual functions) can get rid of this but violates SRP.
- Guideline: Prefer software design that allows the addition of types or operations without the need to modify existing code
- Subtypes must be substitutable for thier base type.
- If you inherit from a base class make sure you keep the contracts of the base types. You guarantee that the behavior of base type is not broken.
Behavioral subtyping (aka “IS-A” relationship)
- Contravariance of method arguments in a subtype
- Covariance of return types in a subtype
- Preconditions cannot be strengthened in a subtype
- Postconditions cannot be weakened in a subtype
- Invariants of the super type must be preserved in a subtype
- Many client specific interfaces are better than one general-purpose interface
- Stratergy Pattern
class Circle;
class Square;
class DrawCircleStrategy
{
public:
virtual ~DrawCircleStrategy() {}
virtual void draw(const Circle &circle) const = 0;
};
class DrawSquareStrategy
{
public:
virtual ~DrawSquareStrategy() {}
virtual void draw(const Square &square) const = 0;
};
-
The Dependency Inversion Principle (DIP) tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
-
Prefer to depend on abstractions (i.e. abstract classes or concepts) instead of concrete types
- The SOLID principles are more than just a set of OO guidelines
- Use the SOLID principles to reduce coupling and facilitate change
- Separate concerns via the SRP to isolate changes
- Design by OCP to simplify additions/extensions
- Adhere to the LSP when using abstractions
- Minimize the dependencies of interfaces via the ISP
- Introduce abstractions to steer dependencies (DIP)
- Used to construct complex objects that are initialized in multiple stages
- Seperates the algorithm from the object structure which is the data of the algorithm.
- Adds new operations to the classs hierarchy without modifying the classes themselves.
- Uses double dispatching.
One, deterministic, automatic, symmetric, special member function with
- No name
- No parameters
- No return type
Designed to give last rites before object death.
Guarantee that derived classes get cleaned up if delete
on a Base*
could ever point to Derived*
.
Rule of thumb: Reverse order of constructoin. Specifically:
- Destructor body
- Data members in the reverse order of declaration.
- Direct non-virtual base classes in reverse order.
- Virtual base classes in reverse order.
- Destructors can be called directly
- Very powerfull for custom memroy scenarios
- Use-cases
- Paired with placement new
- std::vector
- Custom allocators
- Best destructor is no defined destructor. Even the default. Only defulting the destructor will not generate implicit copy and move constructors.
- Avoid calling public functions in destructors.
- Put any resource that needs to be released in its own object(RAII)
- Dtors are called only for fully constructed objs. If ctor throws, object is not fully contructed.
- If there are virutal functions in a class it also should have a virtual public destructor.
- Virtual fucntions are not virtual inside constructors or destructors. Don't call virtual functoins in ctors or dtors.
- Destructors should never throw.
- Slides
- A destructor is used in a class to clean up all the resources that it created and used during its life time.
- If a resource managing class has a destructor and is also copyable, it should have copy constructor and copy assignment in order to get rid of double deletion issues.
- If your class directly manages some kind of resource (such as a new’ed pointer), then you almost certainly need to hand-write three special member functions:
- A Destructor to free the resource
- A Copy Constructor to Copy the resource
- A Copy Assignment Operator to free the left-hand resource and copy the right-hand one
- Use the copy-and-swap idiom to implement the assignment.
NaiveVector &NaiveVector::operator=(const NaiveVector &rhs) { NaiveVector copy(rhs); copy.swap(*this); return *this; }
- If your class does not directly manage any resource, but merely uses library components such as vector and string, then you should strive to write no special member functions.Default them all!
- Let the compiler implicitly generate all rule of 3 special funcitons.
There are two kinds of well-designed value-semantic C++ classes:
- Business-logic classes that do not manually manage any resources, and follow the Rule of Zero
- They delegate the job of resource management to data members of types such as std::string
- Resource-management classes (small, single-purpose) that follow the Rule of Three
- Acquire the resource in each constructor; free the resource in your destructor; copy-and-swap in your assignment operator
- If your class directly manages some kind of resource (such as a new’ed pointer), then you may need to hand-write five special member functions for correctness and performance:
- A Destructor to free the resource
- A Copy Constructor to Copy the resource
- A move constructor to transfer ownership of the resource
- A Copy Assignment Operator to free the left-hand resource and copy the right-hand one
- A move assignment operator to free the left-hand resource and transfer ownership of the right-hand one
NaiveVector &NaiveVector::operator=(const NaiveVector &rhs)
{
NaiveVector copy(rhs);
copy.swap(*this);
return *this;
}
NaiveVector &NaiveVector::operator=(NaiveVector &&rhs)
{
NaiveVector copy(std::move(rhs));
copy.swap(*this);
return *this;
- Write just one assignment operator and leave the copy up to the caller.
NaiveVector &NaiveVector::operator=(NaiveVector copy) { copy.swap(*this); return *this; }
class A {
~A(); // Destrutor
A(A const& rhs);
A(A && rhs);
A& operator=(A const& rhs);
A& operator=(A && rhs);
}
Class | ~A() | A(A const& rhs) | A(A && rhs) | A& operator=(A const& rhs) | A& operator=(A && rhs) |
---|---|---|---|---|---|
unique_ptr | Calls delete on raw ptr | DMS. deleted | Transfers the ptr. Null the rhs | DMS. deleted | Calls delete on lhs ptr. Transfers the rhs ptr to lhs. Nulls rhs. |
shared_ptr | decrement refcount. Clean if zero. | increment refcount | Transfer ownership. No change to refcount. | decrement old refcount. Increment the new refcount. | decrement old refcount. Transer the ownership |
unique_lock | unlock mutex | DMS. deleted. | Transfer ownership. No change to mutex | DMS. deleted. | unlock old mutex. Transfer ownership. |
ifstream | Calls close on the handle. | deleted. | Transfer handle and buffrer content. | deleted. | Closes old. Transfers handle and buffer |
- Lvalues are any memory address with a name.
- Rvalues doesnt have a name. They represent objects that are no longer needed at caller side.
std::move()
unconditionally casts it input to Rvalue reference.- It doesn't move anything.
template <typename T> std::remove_reference_t<T> && move(T &&t) noexcept { return static_cast<std::remove_reference_t<T> &&>(t); }
- Default move operations are generated if no destructor or copy operations is user defined.
- Default copy operations are generated if no move operations is user defined.
- Note: =default and =delete count as user-defined
template< typename T >
void f( T&& x ); // Forwarding reference
auto&& var2 = var1; // Forwarding reference
- Forwarding references represent
- ... an lvalue reference if they are initialized by an lvalue;
- ... an rvalue reference if they are initialized by an rvalue
- Rvalue references are forwarding references if they
- ... involve type deduction;
- ... appear in exactly the form T&& or auto&&.
- Forwarding references use reference collapsing to do its magic.
- The rule is very simple. & always wins. So & & is &, and so are && & and & &&. The only case where && emerges from collapsing is && &&.
- Solves the problem of writing a function which merely forwarding its arguments to another function.
namespace std
{
template <typename T, typename... Args>
unique_ptr<T> make_unique(Args && ... args)
{
return unique_ptr<T>(new T(std::forward<Args>(args)...));
}
} // namespace std
- std::forward conditionally casts its input into an rvalue reference
- If the given value is an lvalue, cast to an lvalue reference
- If the given value is an rvalue, cast to an rvalue reference
- std::forward does not forward anything
template <typename T>
T &&forward(std::remove_reference_t<T> &t) noexcept
{
return static_cast<T &&>(t);
}
- Function template argument deduction was there from C++ 98, Class Template Argument Deduction builds on that and gives the user ability to define templated classes with initialisers without the template argmuent list.
vector<int> {1, 2, 3}; // Prior C++ 17
vector {1,2,3}; // In C++ 17, vector<int> is deduced
- For some scenarios where the compiler fails to deduct the template arguments we can provide deduction guides as well.
- Template specialisations doesn't contribute to overload resolution.
- Know if you are bound by computation or bound by data access.
- Prefer contiguous data in memory.
- Prefer constant strides to randomness.
- Keep data close together in space.
- Avoid dependencies between successive computations.
- Avoid hard to predict branches
- Be aware of cache lines & alignment.
- Minimise number of cache lines accessed by multiple threads.
- Don't be suprised by hardware weirdness. (Cache associativity, denormals)
- function templates and class templates existed in C++ prior C++11.
// Function template example template<typename T> T swap(T& a, T& b) { T temp = a; a = b; b = temp; } // class template example template<class T> struct User { T networth; static int id; } template<class T> int User<T>::id = 0;
- Two new kinds of templates.
- alias templates in C++11.
- variable templates in C++14.
- variable templates - exactly 100% same to a static data member of a class template.
template<typename T> struct is_void{ // template with a static data member static const bool value = (some exrpession) } template<typename T> // equivalent variable template const bool is_void_v = (some expresssion)
- Aliasing is same as typedef with different syntax.
typedef std::vector<int> myvec_int; // C++03 alias syntax using myvec_double = std::vector<double>; // C++11 syntax template<typename T> using myvec = std::vector<T>; // C++11 syntax int main(){ static_assert(is_same_v<myvec_int, std::vector<int>>); static_assert(is_same_v<myvec_double, std::vector<double>>); static_assert(is_same_v<myvec<float>, std::vector<float>>); }
- Calling specialisations explicitly.
template<typename T> T abs(T x) { return (x >=0) ? x : -x; } main () { abs<int>('x'); // [T = int] abs<double>(3); // [T = double] }
- Default template parameters.
template<typename T = char *> void add() {} main() { add<int>(); // [T = int] add<>(); // [T = char *] add(); // [T = char* ] }
- Template type deduction for references.
template<typename T> void f(T t) {} template<typename T> void g(T* t){} template<typename T> void h(T& t){} template<typename T> void k(T&& t){} main() { int i = 5; f(i); // --> 1 g(&i); // --> 2 h(i); // --> 3 k(std::move(i)); // --> 4 k(i); // --> 5 const int p = 0; k(p); // --> 6 k(std::move(p)); // --> 7 }
scenario Template deduciton 1 T = int 2 T = int 3 T = int 4 T = int 5 T = int& 6 T = const int& 7 T = const int
Here for 5th scenario reference collapsing happens. We need to have a lvalue reference inside the function parameter after T's type is deducted. This is because the input parameter is i
and it acts as int&
. In order to have a lvalue reference as a result when the function parameter already has && in it T should be int&.
template<typename T>
void f(T& t){}
int main()
{
int i = 42;
f(static_cast<int&>(i)); // --> 1
f(static_cast<int&&>(i)); // --> 2
f(static_cast<volatile int&>(i)); // --> 3
f(static_cast<volatile int&&>(i)); // --> 4
f(static_cast<const int&>(i)); // --> 5
f(static_cast<const int&&>(i)); // --> 6
}
scenario | Template deduciton |
---|---|
1 | T = int |
2 | Compile time error. Cannot deduce T to become int&& |
3 | T = volatile int |
4 | Compile time error. Cannot deduce T to become volatile int&& |
5 | T = const int |
6 | T = const int special case to support backward compatibility |
- Prefix the definition with
template<>
, and then write the function definition as if you were using the specialisation you want to write.
template<typename T>
struct is_void {
static constexpr bool value = false;
}
template<> // Specialisation
struct is_void<void> {
static constexpr bool value = true;
template<typename T>
T abs(T x) {}
template<> // Specialisation 1
int abs<int>(int x){}
template<> // Specialisation 2
int abs<>(int x){}
template<> // Specialisation 3, often used
int abs(int x){}
// primary template
template<typename T>
constexpr bool is_array = false;
// These are partial specialisations
template<typename Tp>
constexpr bool is_array<Tp[]> = true;
template<typename Tp, int N>
constexpr bool is_array<Tp[N]> = true;
// this is a full specialisation
template<>
constexpr bool is_array<void> = true;
- If you want a partial specialisation then delegate all the work to a class template and partially specialise it.
template<typename T> // Primary class template
class is_pointer_impl{
static bool _(){return false;}
};
template<typename Tp>
class is_pointer_impl<Tp*> {
static bool _(){return true;}
};
template<typename T>
bool is_pointer(T x){
return is_pointer_impl<T>::_();
}
-
Defining a variadic template function
template<typename... ARGS> // --> any number of types void fun(ARGS... args) // --> any number of arguments { println(args...); // -> expand parameters as arguments }
-
Syntax uses ... (ellipsis) symbol in serveral places.
place meaining example In front of a name define name as a place holder for a variable number of elements. typename... ARGS
After a name expand the name to a list of all elements it represents. args...
between two names define the second name as a list of parameters given by the first. ARGS... args
-
Variadic terminology
template<typename... T> // --> template type parameter pack void funct(T... args) // --> function parameter pack { x(args...); // --> pack expansion }
- Pack expansion can be done after an expression using the parameter pack
- special version of sizeof...(pack) yields n-of-args
-
Implementing a variadic template function
- Key is recursion in the definitions
- base case with zero arguments
- recursive case with 1 explicit argument and a tail consisting of a variadic list of arguments
void print(){} // base case template<typename T1, typename... T> void print(T1 t1, T... args) { cout << t1; if(sizeof ...(args)) cout << ", "; print(args...); //recurse on tail }
- Key is recursion in the definitions
- Used in
std::tuple
which is a means to pass parameter packs around as a single element.auto tup = std::make_tuple(1, 2, "three"); int a; double b; string s; std::tie(a, b, s) = tup;
- use ... in template template parameters.
- template with std::vector as return container
template<typename T1, typename... T> auto getContianer(T1 t, T... args) { std::vector<T1> vect {t, args...}; return vect; } int main() { // Create a container from any number of given of elements. Elements can be of any type and container is defaulted to vector but can be changed. auto intCont = getContianer(1, 2, 3, 4); auto charCont = getContianer('1', '1', '1'); return 0; }
- template return container for any container type
template<template<typename, typename...> class ContainerType = std::vector, // template template paramerter defaulted to std::vector typename T1, typename... T> auto getContainer(T1 t, T... args) { ContainerType<T1> cont = {t, args...}; return cont; } int main() { // Create a container from any number of given of elements. Elements can be of any type and container is defaulted to vector but can be changed. auto intCont = getContianer(1, 2, 3, 4); auto charCont = getContianer<std::list>(1, 2, 3, 4); cout << typeid(intCont).name() << endl; // prints St6vectorIiSaIiEE cout << typeid(charCont).name() << endl; // prints St4listIiSaIiEE return 0; }
- template with std::vector as return container