Skip to content

ranadewa/CPP-Talks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CPP-Talks

Repository to note important points on C++ talks

2020

slides

Types

  • pair
  • tuple
  • optional
  • variant

slides

Things to avoid on the fast path

  • 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 Polymorphism vs Variant

Memory Allocation and Allocators

  • 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.

Memory fragmentation

  • 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

Memory Access Performance

  • 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 Caches

  • 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.

    • In memory data access begin with loading data from main memory to the cache.

    • Eviction is the process of removing memory from cache wich is unused for some time.

    • CPU data prefeter can recognise memory access patterns for the program and pre load the data before it is required by the program.

  • 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.

Exceptions

How they work

  • 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.

When to use exceptions

  • 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)

When not to use them

  • 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

How to use exceptions properly

  • Build on the std::exception Hierarchy
  • Throw by Rvalue
    throw std::exception();
  • Catch by reference

Exception Safety Guarantees

  • 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

Functions that should not fail

  • Destructors
  • Move operations
  • Swap Operations

Thread safe Static initialisation

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)

Initialize a member with once_flag

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_.

Comparison of C++11’s primitives

C++17 shared_mutex (R/W lock)

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.

Synchronization with std::latch C++ 20

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");

Synchronization with std::barrier C++ 20

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();

Comparison of C++20’s primitives





  • Single Responsibility
  • Open Close
  • Liscove's Substitution
  • Interface Segregation
  • Dependency Inversion

Single Responsibility

  • 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

Open Close Principle

  • 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

Liscov's Substitution Principal

  • 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

Interface Segregation

  • 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;
};

Dependency Inversion Principal

  • 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.
  • Model Veiw controller

  • Prefer to depend on abstractions (i.e. abstract classes or concepts) instead of concrete types

Summary

  • 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)

2019

slides

Builder Pattern

  • Used to construct complex objects that are initialized in multiple stages

Visitor

  • 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.

Definition:

One, deterministic, automatic, symmetric, special member function with

  • No name
  • No parameters
  • No return type
    Designed to give last rites before object death.

Virtual Destructors

Guarantee that derived classes get cleaned up if delete on a Base* could ever point to Derived*.

Order of Destruction

Rule of thumb: Reverse order of constructoin. Specifically:

  1. Destructor body
  2. Data members in the reverse order of declaration.
  3. Direct non-virtual base classes in reverse order.
  4. Virtual base classes in reverse order.

Explicit Destructors

  • Destructors can be called directly
  • Very powerfull for custom memroy scenarios
  • Use-cases
    • Paired with placement new
    • std::vector
    • Custom allocators

Recommentdations

  • 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.

Slide

  • 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.

The Rule of Three

  • 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;
    }

The Rule of Zero

  • 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.

Prefer Rule of Zero when possible

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

The Rule of Five

  • 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;

By-value assignment operator?

  • Write just one assignment operator and leave the copy up to the caller.
      NaiveVector &NaiveVector::operator=(NaiveVector copy)
    {
        copy.swap(*this);
        return *this;
    }

Examples of Resource Management

  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

Basics

  • 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);
     }

Special member function generation rules

  • 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

Forwarding References

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

The Mechanics of std::forward

  • 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);
}

2018

  • 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.

slides

  • Template specialisations doesn't contribute to overload resolution.

2017

Slides

2016

  • 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)

Slides P1 P2

  • 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

Defining a template specialisation

  • 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){}    

Partial Speciliasation

// 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;

How to partially specialise a function right

  • 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>::_();
}

Variadic Templates

Slides

Variadic function templates

  • 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 
    }

Variadic class templates

  • 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;
      }

About

Repository to note important points on C++ talks

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published