Nuts and Bolts of C++ (2/2)

A curated list of C++ programming basics..

Sunnyvale, December 23, 2021

Modern C++

  1. Rules of template type deduction: (1) the reference-ness of the argument is always stripped off; (2) for a universal reference T&&, an lvalue argument yields T& as the deduced type, and an rvalue argument yields T as the deduced type; (3) if the parameter is neither a reference nor a pointer, the cv-qualifier of the argument is also ignored.
  2. The only difference between auto and template type deduction is that auto assumes that a braced initializer represents a std::initializer_list, but template type deduction doesn’t.
  3. auto in a function return type or a lambda parameter (C++14) implies template type deduction, not auto type deduction.
  4. Use the trailing return type when the function template’s parameters need to be used to specify the return type (C++11).
  5. decltype() determines the declared type of an expression without evaluating it. std::declval<T>() can be used to instantiate type T in an expression passed into decltype().
  6. Use decltype(auto) instead of auto to make the return type a reference (C++14). This is especially useful when the return type of a function template depends on the parameter types (universal references).
  7. If an lvalue expression other than a name has type T, decltype reports that type as T&. For example, if decltype(x) is int, decltype((x)) is int&.
  8. std::vector<bool>::reference, as an invisible proxy class, offers the illusion that operator[] for std::vector<bool> returns a reference to a bit, which might make auto deduce the “wrong” type. (Another example: Eigen vector arithmetics)
  9. Braced initialization prevents narrowing conversions and is immune to C++’s most vexing parse. However, it will be matched to std::initializer_list parameters if at all possible, even if other constructors offer seemingly better matches. So be cautious when you want to call std::set<T>::insert(T&&) with a default value instead of std::set<T>::insert(std::initializer_list<T>).
  10. The type std::nullptr_t implicitly converts to all raw pointer types.
  11. Type traits transformations: std::remove_const<T>::type is a typedef (C++11), while std::remove_const_t<T> is an alias template (C++14).
  12. Scoped enums can be forward declared and its underlying type is int if not specified.
  13. Any function may be deleted, including non-member functions and template instantiations.
  14. A parameter is always an lvalue, even if its type is an rvalue reference.
  15. std::move doesn’t move anything at runtime, but unconditionally casts its argument to an rvalue.
  16. std::forward<T> doesn’t forward anything at runtime, but casts its argument to an rvalue if the argument is bound to an rvalue.
  17. Reference collapsing: if either of the original references is an lvalue reference, the result is an lvalue reference; otherwise it’s an rvalue reference. That happens in template instantiation, auto type generation, typedefs, alias declarations and decltype.
  18. A parameter is a universal reference if it induces type deduction for type parameter T and has the exact form of T&&, rather than something like std::vector<T>&& or const T&&.
  19. If the type argument of std::forward is an lvalue, it returns an lvalue; if the type argument is either a non-reference or an rvalue, it returns an rvalue by virtue of reference collapsing. So if T&& param is a universal reference, std::forward<T> and std::forward<decltype(param)> should be the same.
  20. Perfect forwarding and pack expansion for variadic parameters: foo(std::forward<Args>(args)…);
  21. Don’t apply std::move to a local variable or by-value parameter being returned, because it might hinder the return value optimization (RVO).
  22. Overloading on universal references almost always leads to the universal reference overload being called more frequently than expected. (Example: perfect-forwarding constructors hide copy constructors)
  23. Lambdas with default by-value capture do not capture objects of static storage duration by value.
  24. Use init capture (generalized lambda capture) to move an object into a closure (C++14).
  25. A lambda needs to be “mutable” in order to mutate a variable captured by value.
  26. Define a function alias as constexpr auto&& to avoid unnecessary indirections.

C++ Standard Library

  1. The standard template library (STL) consists of containers, iterators and algorithms. Iterators, as a concept, decouple containers and algorithms.
  2. For some implementations, std::list::size() takes linear time to support constant-time splicing operation.
  3. The erase-remove idiom for std::vectors, std::deque’s and std::string’s: c.erase(std::remove(c.begin(), c.end(), value), c.end());
  4. To remove something pointed by a reverse iterator ri: c.erase(std::next(ri).base());
  5. std::set::find and std::map::find are based on equivalence check instead of equality check.
  6. References are not copyable and thus not able to be put in a standard container. Use std::reference_wrapper instead.
  7. Relocation invalidates all the iterators, references and pointers in a contiguous-memory container.
  8. Rehashing invalidates all the iterators in an unordered associative container, but references and pointers are unaffected.
  9. Insertion doesn’t invalidate iterators in a node-based container (e.g. lists, sets, maps).
  10. Erasure won’t make a contiguous-memory container to resize or an unordered associative container to rehash.
  11. Use node-based associative containers only when pointer stability is needed, otherwise use contiguous containers for better performance.
  12. Heterogenous lookup means looking up a key that has a different type than that in an associative container.
  13. The pointer (T*) returned by allocator<T>::allocate() points to a chunk of allocated memory, not a constructed object.
  14. A std::list<T> does not need allocator<T> to allocate memory, but needs allocator<T>::rebind<ListNode<T>>::other to allocate memory.
  15. Allocators are supposed to be stateless, so that one allocator can deallocate the memory allocated by another allocator of the same type.
  16. std::exception’s include (1) runtime errors, (2) logic errors and (3) language errors.
  17. Use std::exit() for normal endings or expected failures, and use std::terminate() for unexpected errors.
  18. std::unique_ptr<Derived> implicitly converts to std::unique_ptr<Base>, and the same applies to std::shared_ptr.
  19. std::unique_ptr<T> implicitly converts to std::shared_ptr<T>.
  20. A std::shared_ptr consists of a raw pointer to the resource and a pointer to the control block for the resource. The control block contains an atomic reference count, a weak count, and optionally, the custom deleter and allocator of the resource.
  21. An advantage of std::make_shared is that it allocates a single chunk of memory for both the resource and the control block.
  22. Make sure to construct the resource-managing object (e.g.unique_ptr or shared_ptr) immediately after acquisition of the resource (e.g. use of new) to prevent exceptions happening between them.
  23. The custom deleter affects the type of a std::unique_ptr, but not a std::shared_ptr. Unlike std::unique_ptr, std::shared_ptr can’t support built-in arrays.
  24. Do not pass this into a std::shared_ptr. Instead, make the class inherit from std::enable_shared_from_this<T> and create the shared pointer from shared_from_this() (an application of CRTP).
  25. When using std::unique_ptr with std::default_delete to manage the PImpl object, define special member functions of the handle class in the implementation file after the definition of the PImpl class, even if default implementations are adopted. Otherwise, the delete operator invoked in the implicit inline destructor cannot be instantiated without knowing the size of the incomplete implementation object.
  26. std::function makes its own copy of the underlying callable object.
  27. Function pointers can only point to functions, but std::function can point to any callable objects.
  28. Pass STL iterators, std::function’s and std::initializer_list’s by value rather than by reference.
  29. Salting is the practice of injecting some (random) hidden state into a hash function to defend against preimage attacks.

Concurrency in C++

  1. Compared with std::async, the std::thread API offers no direct way to get return values from asynchronously running functions; and if those functions throw, the program is terminated.
  2. The default launch policy of std::async is either std::launch::async or std::launch::deferred, meaning it permits both asynchronous and synchronous task execution.
  3. std::packaged_task is a wrapper around a callable object for running it asynchronously (with std::thread) and pushing the return value or exception into the shared state of a std::future.
  4. Invoking the destructor of a joinable thread terminates the program, so be sure to join or detach a thread on every path, even when an exception is thrown.
  5. Invoking join() or detach() on an unjoinable thread yields undefined behavior, so be sure to check joinable() before joining or detaching it.
  6. std::future<T>::get() can only be called once, and will throw the same exception that the asynchronous thread has failed to catch.
  7. Wrap a std::promise with std::ref() to pass it into the std::thread constructor by reference.
  8. Use “volatile” to disable compiler optimizations on reads and writes on special memory, such as memory-mapped I/O. “volatile” is not a feature for concurrent data access since it provides neither atomicity nor a specific order.
  9. Use std::unique_lock to support manual unlocking, std::condition_variable, and std::defer_lock. Otherwise, use std::lock_guard instead.
  10. Use std::lock on multiple lockable objects (either with std::lock_guard and std::adopt_lock, or with std::unique_lock and std::defer_lock) to avoid deadlock.
  11. Use std::recursive_mutex to allow locking the same mutex multiple times in the same thread.
  12. Achieve thread-safe lazy initialization by using (1) function-level static initialization or (2) std::call_once() and std::once_flag.
  13. Condition variables in general have the problem of spurious wake-ups, so we need to check the required condition in std::condition_variable::wait().
  14. For simplicity, always hold the lock when calling std::condition_variable::notify_{one,all}(), although it’s not necessary in all cases.
  15. The default constructor of std::atomic does not initialize the object completely, and a subsequent std::atomic_init() needs to called.
American White Pelicans

References:

  1. Scott Meyers. Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14
  2. Scott Meyers. Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library
  3. Nicolai Josuttis. C++ Standard Library, The: A Tutorial and Reference (2nd Edition)

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store