CPPCON 2019: Move Semantics - Klaus

Talk: Back to Basics: Move Semantics

  1. std::move and rvalue reference are ways to tell compilers to “move” the content from one to the other

    1. Note: the reason that T&& is a lvalue is because even though caller explicitly moves some content into a function, for example, the designer of the function can still do many stuff inside this function. Therefore, they should ensure they std::move the rvalue reference again to trigger move constructor/assignment
  2. std::move unconditionally casts T into a rvalue reference

    // possible implementation of std::move
    template<typename T> std::remove_reference_t<T>&& move(T&& t) noexcept
    {
    	return static_cast<std::remove_reference_t<T>&&>(t);
    }
    
  3. Default Move and Copy

    1. The default move operations (move constructors/assignments) are generated if no copy operation or destructor is user-declared
      1. Note: Klaus uses “user-defined” instead of “user-declared”. However, in cppreference, it uses the word “user-declared”.
    2. The default copy operations are generated if no move operation is user-declared
      1. They “prefer” copy operations
    3. Note: =default and =delete count as user-declared!
  4. The default behavior of move/copy leads to the rule of 0 or rule of 5

    1. Either you define nothing (only constructors) or You define everything (destructors, copy constructors, copy assignment, move constructors, move assignment)
    2. You don’t want your user to memorize these default behaviors
  5. Forwarding References (Universal References)

    1. Used in std::move implementation
    2. Forwarding References in this exact form: T &&. Even const T&& is not considered as universal reference
    3. How can we create a function that takes in anything (lvalue, rvalue), and forward them as their original type?
      1. The problem arises because the rvalue reference is lvalue

      2. std:: forward conditionally casts its input into an rvalue reference: it takes in an lvalue and forward them based on their original type

      3. How it works

        1. The T in universal reference is deduced as T& if lvalue and T is rvalue
        2. We call std::forward<T>(arg) as this, so after casting T& is T& and T is T&&, which is what we expect
        template<typename T>
        T&& forward(std::remove_reference_t<T> &t){
        	return static_cast<T&&>(t);
        }
        

CPPCON 2019: Exception Handling - Ben

Talk: Back to Basics: Exception Handling and Exception Safety

  1. Ideas behind Exception Handling: separate the error reporter from error handler

  2. Traditional C-style error handling

    1. check return value
    2. check errno
    3. Drawbacks: if the caller cannot handle this, they need to propagate the error code back in their return value, which somewhat interferes with the normal return path (my point)
  3. C++ EH separates this from the normal control flow by providing an exceptional control flow with

    1. throw
    2. try
    3. catch
      1. Idiomatic way to catch exception: catch(T &arg){}. Use a reference of type T here.
  4. Exception hierarchy

    1. In <exception>, the base exception is defined, which specifies the base class used by all exceptions in STL
    2. In <stdexcept>, common exceptions like std::out_of_range and std::runtime_error are defined
  5. Behaviors

    1. Once a function is throw-ed, we try to find a function in previous stack frame that can handle the exception
    2. If no such function exists and we are out of main, std::terminate is called
    3. If before catching the exception, we encounter another exception (e.g. some destructors throw during stack unwinding), std::terminate is called.
    4. Failure in Constructors
      1. If an exception is thrown in constructor, it’s like the object doesn’t exists
      2. So, you need to release any resources that are allocated before this exception happens
      3. RAII can help now
  6. Exception Safety

    1. Basic
      1. Function may throw an exception
      2. Program’s state may change, but it’s still in consistent state (no memory leak, no invalid state)
    2. Strong
      1. Program’s state remains unchanged
      2. To achieve strong guarantee, we could
        1. First, do everything that could throw and store their result in local variable
        2. Update the state of the program, using operations that will not throw
    3. No Throw
      1. noexcept
      2. This is not enforced at compile time but compiler does optimization based on this (see “The cost of Exception” below) and some STL actually relies on this
        1. If your move constructor/assignment is not defined with noexcept, containers like vector prefers the copy constructors/assignment as they want a strong exception guarantee (they want the original data be consistent when they try to realloc their spaces)
      3. If a function with noexcept throws, std::terminate is called
      4. Some functions should never throw
        1. destructors
        2. operator delete
        3. swap (+ move)
        4. Operations on primitive types (my point)
  7. The cost of Exception (My Point)

    1. It should only be thrown if the condition is exceptional (because it has a cost related to it)
    2. Cost of try and catch
      1. When you have try and catch, compilers will leave a path that calls a function that does stack unwinding for you
        1. The standard doesn’t mandate when stack unwinding happens, they usually happen when the function that could handler the exception is found
        2. If you declare all your functions inside try block with noexcept, compilers can optimize out this hidden path as it knows now your function will never throw
    3. Cost of throw
      1. You need to call library function that actually does throw for you
      2. Then, an iterative process of finding appropriate function that handles your exception occurs

CPPCON 2019: Smart Pointers - Arthur

Talk: Back To Basics: Smart Pointers

  1. unique_ptr

    1. Custom Deleter (a lambda)
      1. You can use it to manage fopen/fclose and generally everything that provides you with a constructor and destructor like stuff (useful when interfacing with system resources)
      2. Need to use traditional functor here or stateless lambdas are default constructible (since C++20)
    2. Code smell: passing a pointer by reference is usually a code smell
      1. If you want to denote ownership transfer, pass by value
  2. shared_ptr mechanism

    1. A control block (an atomic reference count, weak ptr count, deleter and actual pointer to the object) + An actual object
      1. shared_ptr
    2. Aliasing constructors
      1. The “ptr to T” can point to some elements in classes (int in vector<int>) or point to subclasses. This is what shared_ptr.get() returns.
      2. They are irrelevant to the actual pointer being destructed. They only contribute the the reference count
      3. We could declare those shared pointer like this: std::shared_ptr new_ptr = std::shared_ptr(another_shared_ptr, some_address). another_shared_ptr is the one that you share the ownership with (the control block)
    3. Potential optimization of make shared (all about locality)
      1. Fewer call to new
      2. make_shared can enable the allocation to allocate the control block near the actual object which is beneficial in terms of locality
  3. weak_ptr

    1. A tickets to get shared_ptr
      1. If object is deleted, throw an exception (directly constructing a shared_ptr) or return nullptr (lock)
    2. Share the control block and Contribute to the weak reference count
      1. As soon as reference count + week ref count != 0, we will not destroy the control block
  4. enable_shared_from_this

    1. Conspire with std: if initialized by shared_ptr, we initialize the weak_ptr inside
    2. Useful for async where you give your ownership to someone

What’s Next

  1. RAII and The rule of 0
  2. Lambda
  3. Type erasure