Skip to content

Brushup January 2022

Item 17: Understand special member funcion generation

  • Special functions: constructor; destructor; copy constuctor; copy assignment; move constructor; move constructor.
  • General rule: A default form is generated automatically by the compiler if called; the default functions are public and inline; the default functions reverberate to member data and base classes if needed; if one of the special member functions is defined, the default version for this function is not generated; the explicit definition of one special function does not affect the automatic generation of the others (this is deprecated in C++11. It still works, but probably won't in future versions. It is also true if a destructor is explicitly declared. Use =default).
  • Special rule for move (1): the move member functions are not independent. That is, if the move constructor is defined, there is no way that the compile will generates the default version for the move assignment.
  • Special rule for move (2): No default move operation is generated if any copy operation is explicitly defined.
  • Special rule for move (3): C++11 does not generate default move operations if a destructor is explicitly defined.
  • The rule of three: It is a good practice to: whenever define one of the copy constructor; copy assignment; or destructor, to define all the three. That is because it is likely the case that a special implementation for any of these likely implies an special implementation for the others.
  • The rule of five:

Common case for base classes

class base {
public:
 virtual ~Base() = default;

 Base() = default;

 Base(const Base&) = default;
 Base& operator=(const Base&) = default;

 Base(Base&&) = default;
 Base& operator=(Base&&) = default;
- It is a good idea to explicitly declare the copy and move operations as default. Think about the case of logging at destruction. You created a destructor, then move operations are not longer generated. In C++11, the copy operators are generated, so a move is transformed in a copy, and suddenly we have a degradation in performance.

CPP-Conf-20. Back to Basics: Design Patterns - Mike Shah

  • Gang of four book: Design patterns: https://en.wikipedia.org/wiki/Design_Patterns
  • Design pattern in games book: https://gameprogrammingpatterns.com/
  • Three categories: Creational; Structural; Behavioural.
  • Singleton: The example given for using the Singleton pattern was not very good. The same could have been accomplished by setting constants in a namespace.
  • Singleton: I believe that the general case for this pattern is the following. If you need some sort of initialization before consuming members and this initialization should happen just once, then you have the case for a Singleton pattern.
  • Adapter pattern: It is like a wrapper. It is used to create a new interface from some other well stablished interface. Maybe you want to hide or to extend some functionalities.
  • Iterator pattern: This one is recommended for lazy containers; or if you plain to have multiple types of containers.

CPP-Conf-20. Back to Basics: Templates Parts 1 and 2 - Andreas Fertig

Item 15: Use constexpr whenever possible

  • constexpr in objects: Signal that its values are constant and known at compilation time.
  • constexpr functions: If all the arguments of this function are known at compilation time, then the return value of this function is computed at compilation time! If there is at least one of the arguments that is not known at compilation time, then it behaves as a normal function and the return value is computed at runtime.
  • In C++11, constexpr functions can contain only a single executable statement. That is, a return statement.
  • In C++14, this constraint is relaxed. The only restriction there (which is also a restriction in C++11) is that you are limited to return literal types, that is, types that have their values determined during compilation.
  • You can have user-defined literal types because constructors and other member function can be declared constexpr.
  • Nice example (considering that constuctor and getters of Point are constexpr.
    constexpr Point midpoint(const Point& p1, const Point& p2) noexcept{
        return { (p1.xValue() + p2.xValue()) / 2,
                 (p1.yValue() + p2.yValue()) / 2 };
    }
    
    constexpr mid = midpoint(p1,p2);
    
  • Having the ability to do some computation during compilation time is of particularly importance for programmers of embedding systems.
  • In C++14, also the setter member functions can be declareed constexpr. They could not in C++11 because there we have that constexpr functions were implicitly const; and also that void (the return type of a setter) is not considered a literal type.

Item 16: Make const member functions thread safe

  • mutable keyword: This keyword is use to classify data members that can have this value updated even if this happens inside a const function or if the object that contains is was declared as const.
  • A std::mutex is a move-only type.
  • Because of mutable we may have const functions that are actually not thread-safe, and this could lead to problems. That's the reason behind this item.

CPP-Conf-20. Collaborative C++ development with VSCode

Item 41: Consider pass by value for copyable parameters that are cheap to move and always copied

  • This, by no means, overrules the wisdown that you should prefer to pass arguments by reference.
  • For copyable, cheap-to-move arguments that are always copied, pass by value may be nearly as efficient as pass by reference. The advantage is that you may have to write just one function, therefore, less object code and less code to maintain.
        //This is the special case mentioned above. `s` is alwways copied both for
        lvalues and rvalues. The extra cost incurred is the one of a move.
        void addName(std::string s){
            v.push_back(std::move(s));
        }
    

Item 42: Consider emplacement instead of insertion

Fundamental example

vector<string> vs;  
vs.push_back("xyz");  
vs.emplace_back("xyz"); //More efficient.  

The string literal is used as the argument in the constructor of the element being included in the vector. In the push-back version, a temporary object is created (such that it can be passed as a rvalue-reference). Then, inside push_back, is used again to construct the object that will go inside the container. emplace_back, on the other hand, uses perfect forwarding.

Difference between copy initialization and direct initialization.

MyClass{
    MyClass(std::string s)
};

MyClass x = "abc"; //copy initialization
MyClass x("abc");  //direct initialization

If the constructor was explicit, the copy initialization would not be allowed. The explicit prevents implicit conversions, that are done in copy initialization.

Item 21: Prefer std::make_unique and std::make_shared to direct use of new

Using new potentially lead to memory leak.

process( std::shared_ptr<Widget>(new Widget), computePriority() )

How? You may ask. This is a compiler-related issue. Before calling a function, the arguments must be resolved. It could be the case that the compiler decides to execute the code in the following order: 1. new Widget() 2. computePriority() 3. std::shared_ptr constructor

If computePriority throws an exception, we have a leak because the dynamicaly allocated pointer created in the first instruction was not wrapped into a smart pointer.

Sometimes new is necessary

There are some edge cases in which the use of new may be necessary. For example, if you need to pass a custom deleter. In this case, simply create the smart pointer in an isolated instruction.

auto sp = std::shared_ptr<Widget>(new Widget(), customDeleter);
process(sp,computePriority);
process(std::move(sp),computePriority); //even better!    

CPP-Conf-20. Template Metaprogramming: Type traits

  • Mandatory video: Cpp-Con 2014 Template tutorial by Walter E. Brown.

Metafunctions

Metafunctions are class templates which are supposed to return a value. Such a value is by convention stored as a static const expr member called value, if we are talking about value metafunctions, or in a type called type, if we are talking about type metafunctions.

template<typename T>
struct{
    using type = T;
};

C++17 feature

In C++17 we have auto non-type template parameters

template<auto X> //before template<typename T, T X>
struct ValueIdentity{
    static constexpr auto value = X;
};

Community convetions

Convention with the advent of Variable Templates

template<auto X>
static constexpr auto ValueIdentity_v = ValueIdentity<X>::value;

We have a similar convention for type metafunctions. As a bonus, typename dance is removed.

Conversion operator

 operator int() const { return 7; } // implicit conversion to int
 explicit int*() const { return nullptr: } // explicit conversion to int*

Basic metafunctions provided by the standard library

  • Cpp17UnaryTypeTrait: must inherit from integral_constant
  • Cpp17BinaryTypeTrait: must inherit from integral_constant
  • Cpp17TransformationTrait

  • integral_constant

template class<class T,T v>
struct integral_constant{
    static constexpr T value = v;

    using value_type = T;
    using type = integral_constant<T,v>;

    constexpr operator value_type() const noexcept{
        return value;
    }

    constexpr value_type operator()() const noexcept{
        return value;
    }

template class<bool B>
using bool_constant = integral_constant<bool,B>

using true_type = bool_constant<true>;
using false_type = bool_constant<false>;
  • is_void metafunction via specialization.
template<typename T>
struct is_void : std::false_type {}

template<>
struct is_void<void> : std::true_type {}

static_assert( is_void<int>{} ) //Internally, it calls the implict conversion operator above of integral_constant
  • remove_const (Transformation trait)
 remove_const<int> -> int
 remove_const<const int> -> int
 remove_const<const volatile> -> volatile int
 remove_const<int *> -> int *
 remove_const<const int *> -> const int *
 remove_const<const int * const> -> const int * 

Primary type categories

Primary type categories and templates: For any given type T, the result of applying one of these templates to T and to cv T (const/volatile) shall yield the same result. The type void is in this category.

  • is_void
  • is_null_pointer
  • is_integral
  • is_floating_point
  • is_array
  • is_enum
  • is_union
  • is_class
  • is_function
  • is_pointer
  • is_lvalue_reference
  • is_rvalue_reference
  • is_member_object_pointer
  • is_member_function_pointer

A word from the standard: You should never implement yourself specialization of these types. One thing more: For any given type T, exactly one of the primary type categories shall yield true_type.

Word about templates

Full specialization:

template <> //empty brackets
struct MyClass<int> // type fully defined

Partial specialization

template <typename T>
struct MyClass<T const> // partially defined (const qualifier). I am using east const notation here.

Conditional type metafunctions

Conditional type metafunction. Return a type based on a condition.

The decltype functioning

It is like telling the compiler: Pretend you call this function. What type does it return? (But don't actually call it).

What about if the function I am calling has parameters? Use declval

template <typename T>
using is_null_pointer = decltype(detail::is_nullptr(std::declval<T>()));

The declval guarantees you are in a context free of evaluation.

Member pointer type

int* is a valid pointer type. Does not have to point to anything

struct Foo{};
int Foo::* // is a member pointer type. Does not have to point to anything

SFINAE: Substitution error is not a failure

template <typename T>
std::true_type can_have_pointer_to_member(int T::*);

template <typename T>
std::false_type can_have_pointer_to_member(...);

static_assert(decltype(can_have_pointer_to_member<int>(nullptr))); //Compiler error
template <typename T>
std::true_type can_have_pointer_to_member(int T::*);

template <typename T>
std::false_type can_have_pointer_to_member(...);

template <typename T>
using can_have_member_ptr = decltype(can_have_pointer_to_member<T>(nullptr));

static_assert(decltype(can_have_member_ptr<int>(nullptr)));  //SFINAE

Rule of thum: Use SFINAE in the return type whenever it is possible.

Item 22: When using the Pimpl idiom, define special member functions in the implementation file

Why Pimpl idiom?

One of the advantages of the Pimpl idiom is to save compilation time for the clients of the class.

Let us say that our class Widget has a dependency on class Gadget. That is, Gadget is a member of Widget. Including the header Gadget.h in Widgets.h will make the compiler to compile Widgets again every time Gadgetis updated.

Using the Pimpl idiom, we wrap all its members to a Widget::Impl class, which is declared as an incomplete type in Gadget. Next, we define Widget::Impl in Widget.cpp

//widget.h
struct Widget{
    struct Impl;
    std::unique_ptr<Impl> pImpl;

    ~Widget();

    Widget(Widget&&);
    Widget& operator=(Widget&&);

    Widget(const Widget&);
    Widget& operator=(const Widget&);
};

//widget.cpp
#include "widget.h"
#include "gadget.h"

Widget::Impl{
    Gadget g;
}

Widget::Widget() : pImpl(std::make_unique<Widget::Impl>()) {}
Widget::~Widget() = default;

Widget::Widget(Widget&&) = default;
Widget::operator=(Widget&&) = default;

Widget::Widget(const Widget& other) : pImpl(std::make_unique<Widget::Impl>(*other.pImpl)){}
Widget::operator=(const Widget& other){
    *pImpl = *other.pImpl;
    return *this;
}

Notice that we declare the delete in the header but we defined it in the cpp. That's because the compiler would generate an error otherwise. The automatic generated destructor is generated inline, in the header file, and at that point Impl is not a complete type yet. The destructor of the unique_ptr will try do delete an incomplete type, and that is not allowed.

Because whenever we specify that we are not using the default delete function, the default move operations are not aumatically generated as well, and that's why we declare and define them ourselves.

Finally, the copy member functions should be specifically declared, otherwise we would have a shallow copy instead of a deep copy of pImpl.

Item 23: Understand std::move and std::forward

What is a move and what is a forwarding

std::move performs an unconditional cast to an rvalue object.

std::forward casts to an rvalue object if the object (created as an input parameter) was passed as an rvalue.

Ephemeral characte of a rvalue

The rvalue character of an object is ephemeral. In a function definition, an rvalue parameter becomes an lvalue parameter in the scope of the function (you need to store the passed parameter somewhere, and every time you store, it is a lvalue).

We use std::forward to recover the rvalue character of the passed parameter (if any) inside if the function scope.

Move and const qualifier

Be aware that move constructors and move operators are not const qualified. That is quite natural, because these functions are supposed to change these objects passed as parameters, so they can't be const qualified.

As a rule of thumb: Never move const objects. You may got a behaviour that is not the expected one. For example, to have the object being copied, instead of being moved.

class MyClass{
    std::string m_text;
    MyClass(const std::string text) : 
      m_text( std::move(text) ) {}  //Does not move. It copies!

    MyClass(std::string text) :
      m_text( std::move(text) ) {} //This moves, indeed.
}

Cpp-conf-21: B2B: Compiling and linking

Pre-processing

  • Headers + Cpp -> PreProcessor -> Translation Unit per .cpp (.i) -> Object file (.o) -> Linking -> Excecutable
  • Object file: "Data" and "MetaData"
  • Metadata:
  • "Text" or "Code": for machine instructions
  • "Literal": initialized read-only data
  • "Data": initialized read-write data
  • "Bss": uninitialized read-write data.
  • When creating the object file, some placeholders that refer to other translation units are created. It is the job of the linker to resolve these placeholders by linking them to the actual place of definition.

One definition rule

  • One Definition Rule: No translation unit may define certain entities more than once.
  • A translation unit is what we've got after preprocessing and including all the headers text in the same file as the source code.
  • Some entities must be defined in exactly one translation unit:
  • non-inline variables
  • non-inline, non-template functions
  • constexpr and consteval functions are implicitly inline
  • It is easier to write a macro that will misbehave than an inline function that will misbehave.
  • C++ Modules perspective is to decreasing build times by its new code organization structure, which is supposed to increase build time efficiency.

The special case of templates

Templates are usually declared and defined in header files. But remember, templates are recipes for class and function generation, not real classes and functions.

An explicit template specialization is, on the other hand, not a recipe, but an existing entity representing a class, a function or a variable. So the ODR rules apply here. That's it, we have the definition of an explicit template specialization separated from its declaration.

  • Explicit Template Instantiation
//file: a.h
//function template definition
template <typename T>
void swap(T &x, T &y){
    T temp(std::move(x));
    x = std::move(y);
    y = std::move(temp);
}

// explicit function instantiation declaration
extern template
void swap<int>(int &x, int &y);

//file: a.cpp
//explicit function instantiation definition
template
void swap<int>(int &x,int &y);

Notice that we don't provide a body in the explicit function instantiation. The compiler will use the body in the template declaration. This technique is used to instruct the compiler to generate the version for ints of the function template swap in the source file a.cpp and not somewhere else. It also instructs the compiler that this should be unique and sole definition for this version to use.

Remember that the compiler might generate the code for a function template several times during the compilation. But these generated code have special metadata instructing the compiler to consider just one of these several definition (pick any flag).

In this sense, entity templates behave as inline functions. They are pasted in several tanslation units and the compiler rids off all of them except one.

Item 24: Distinguish universal references from rvalue references

  • Universal references appear in the context of type deduction. That is, when templates or the auto keyword is involved.
  • Universal references can bind to a rvalue reference but also to a lvalue reference. It can bind to const or volatile variations of them either.
  • To be defined as a universal reference, we must have something similar to
template<typename T>
void f(T&& param)

Anything else discards the possibility of param to behave as an universal reference. That includes cv qualifiers such as const and volatile. When they are present, param is an rvalue for sure.

void f(Widget&& param);     // rvalue reference
Widget&& var1 = Widget();   // rvalue reference

auto&& var2 = var1;         // Universal reference

template<typename T>
void f(std::vector<T>&& param); // rvalue reference

template<typename T>
void f(T&& param);          // Universal reference

template<typename T>
void f(const T&& param)     // rvalue reference

The rule of thumb is: If there is type deduction involved and the param strictly respects the format T&&, then it is a universal reference.

Cpp-Conf-20. C++20 STL Features.

String views

static constexpr array skipped_extensions{".dll"sv, ".exe"sv, ".obj"sv};
static_assert(ranges::is_sortes(skipped_extensions));

erase-remove idiom

v.erase(remove_if(v.begin(),v.end(),pred));
  • In C++20 we have uniform container erasure =D. Simply calling erase_if(v,pred) will work as expected.

C++20 New features

  • atomic_ref: think about a variable that you realize several non-atomic operations, but at some point, you need to do atomic operations on it. Use atomic_ref in this part.
  • span: It is like a stringview. A pointer with length. Is it readonly? It is supposed to be used for contiguous containers such as vector, list and arrays.
  • read more about ranges. C++20 ranges::contiguous_range.
  • read more about string view
  • GH-724: Fix boyer_moore_searcher: Fixed a 43-year-old bug by implementing a 40-year-old fix.

Item 25: Use std::move on rvalue references and std::forward on universal references

Rule of thumb

Always move a rvalue and always forward a universal reference. A rvalue parameter (Widget&& w) is always bound to rvalues, so you should unconditionally cast it to rvalue to preserve its character. Universal references, on the other hand, may have a rvalue character, but may be not. In this case, use forward to preserve its original character and eventually cast it in to a rvalue.

String literals and universal references

//Implementation 1
class Widget {
public:
    template<typename T>
    void setName(T&& newName)  //Universal reference
    { name = std::forward(newName); }
private:
    std::string name;
}

//Implementation 2
class Widget {
public:
    void setName(const std::string& newName)
    { name = newName; }

    void setName(std::string&& newName)
    { name = std::move(newName); }
private:
    std::string name;
}

Widget w;
w.setName("Amazonia");

Let's consider the code generated with respect the two implementations. In the first, a string literal "Amazonia" is passed to the template member function setName. Notice the member function takes an universal reference, that's why we are forwarding.

The deducted type is: const char(&)[9] (or const char*). A string literal is a lvalue, therefore, forward forwards a lvalue. The string literal is directly used as a parameter of the assignment operator.

In the second implementation, the string literal is converted to a string type. Therefore, a temporary string object is created. Then, this same object is passed as a parameter to the assignment operator. In this implementation we spend one string constructor that was not necessary in the first.

// These copy from ROM to RAM at run-time:
char myString[] = "hello";
const int myInt = 42;
float myFloats[] = { 3.1, 4.1, 5.9 };

// These copy a pointer to some data in ROM at run-time:
const char *myString2 = "hello";
const float *myFloats2 = { 3.1, 4.1, 5.9 };

Return Value Optimization (RVO)

The standard says: The compilers may elide the copying (or moving) of a local object (function parameters are not included) in a function that returns by value if (1) the type of the local object is the same as that returned by the function and (2) the local object is what's being returned.

We hinder RVO when writing return std::move(w) on a local variable w because the return type is not anymore the same of the local variable. It is a reference to w. And because of that, RVO should not be applied.

In other words, it may make sense to return forward or return move a function parameter. But doing that in a function local variable hinders RVO. Indeed, compilers are constrained to elide the copy or to apply a move in the return value if the conditions for RVO are met.

Even when the RVO is not met, for example

Widget makeWidget(Widget w)
{
    ...
    return w;
}

This function is not eligible for copy elision (w is a function parameter), but the compiler must treat it as if it was written as:

Widget makeWidget(Widget w)
{
    ...
    return std::move(w);
}

The lesson is: You can't help your compiler by applying a std::move in a return statement involving a local variable but you can certainly hinder them (by precluding the RVO).

  • Read more about RVO and its conditions.

Item 26: Avoid Overloading on universal references

Function taking universal references are the optimal choice sometimes

void logAndAdd(const std::string& name)
{
    auto now = std::chrono::system_clock::now();
    log(now,"logAndAdd");
    names.emplace(name);
}

std::string petName("Darla");
logAndAdd(petName); //emplaces receives a lvalue, thus it copies.
logAndAdd(std::string("Persephone")); //the rvalue is bounded to the parameter, a lvalue, thus emplaces copies.
logAndAdd("Patty Dog"); //the string literal is used to create the string parameter which is also again copied in emplace.
template<typename T>
void logAndAdd(T&& name)
{
    auto now = std::chrono::system_clock::now();
    log(now,"logAndAdd");
    names.emplace(std::forward<T>(name));
}

std::string petName("Darla");
logAndAdd(petName); //emplaces receives a lvalue, thus it copies.
logAndAdd(std::string("Persephone")); //The parameter is a rvalue, thus emplaces moves.
logAndAdd("Patty Dog"); //the parameter is a string literal. Emplaces uses directly the string literal in the string constructor.

Problems start to arise when we try to overload a function that is already taking universal parameters. The reason is that functions taking universal references are the greediest functions in C++.

void logAndAdd(int id)
{
    auto now = std::chrono::system_clock::now();
    log(now,"logAndAdd");
    names.emplace(names_collection[id]);
}

short id = 42;
logAndAdd(42); //Since it is not an int, it calls the universal reference version.

Overload resolution rules

  • An exact match beats a match with a promotion (short to int).
  • The rule above also applies to keuword qualifiers such as const.

The problem caused by overloading a universal reference function is particularly problematic with perfect forwarding constructors.

class Person
{
    template<typename T>
    Person(T&& name) : name(std::forward<T>(name)) {}
}

Person p("Daniel");

Person q(p); //Calls the perfect-forwarding constructor instead of copy constructor.  
             //because `Person(Person& name)` is a better match than `Person(const Person& name)`.            

Cpp-Conf-20: Modern C++ Safety and Security

  • Hard, firm and soft real time requirements. They are defined according to its usefulness level decay against the latencey passed a deadline time.
  • I've never have seen exceptions being used in the embedded context. They are expensive, mainly because of stack uniwinding. But also, if an unhandled exception is thrown, the program goes to the top of the stack and resets the program state.
  • Memory allocation and deallocation are not done in system with hard real time requirements. All memory has already been allocated priorly.

C++20 Features

  • Concepts
  • Ranges
  • Coroutines
  • Modules
  • std::format replaces printf
  • constexpr++
  • using enum
  • volatile deprecation
  • designated initializers (used in aggregates "structs without constructors").
    struct account{
        unsigned int number;
        float balance; //why use float and not double?
        float amount;
    };
    
    account act{.number=16,.amount=300}
    
  • spaceship operator
  • calendar + time zones
  • string literals
  • span

About interfaces

Design it to be easier to use correctly and harder to be used incorectly. What does this say about optional parameters and interface change / extensions?

Concepts

With concepts, we now may have several destructors

constexpr ~Optional() requires (!std::is_trivially_destructible_v<T>) { ... }
constexpr ~Optional() = default;

Ranges

We thought that iterators were safe until we discover they are not (examples?). Ranges solve that.

/instead of
std::sort(vec.begin(),vec.end());

//with ranges
std::ranges::sort(vec)
//it comes with several viewss
for(auto [r,v] : ranges::reverse_view(mp) ){ ... }
//it seems that we should be very careful while using
//the ranges library because it may lead to very unefficient code... though pretty
//the | operator is very expensive!

views::filter(bit_1_mask) | views::reverse | views::transform([])(int n){ return n*3; }
  • It would be nice to follow a didactic video on C++20 features.
  • What about trying to define my function with pre and post conditions. Contracts will allow to do that in C++23.
  • Why people don't like exceptions?
  • Exploiting Modern C++ (Book)

Item 27: Familiarize yourself with alternatives to overloading on universal references

Why use Universal references?

  • We want to take advantage of universal references being binded sometimes to lvalues and sometimes to rvalues. The conditional casting performed by perfect-forwarind allows us to write more concise functions.
  • Sometimes is more efficient. Using const T& does not allow us to use move semantics.

What are the alternatives?

  • Abandon Overloading. That is, define a function with a different name. This is not really an alternative if you are handling constructors, though.
  • Passing by const T&. Your code won't be the most efficient implementation in some occasions. Namely those that involves move semantics.
  • Pass by value. If you need at some point to copy the parameter anyways, pass by value may be the right choice. You can safely use move semantics on it.
  • Tag Dispatch. This technique consists in create a compile time condition. You can maintain the universal reference, but now you have an additional parameter. You then implement one version for each variation of this second parameter. Scott used std::false_type and std::true_type for example.
    To implement TagDispatch you need a dispatcher function and one or more implementations. Possibly overloaded, being the TagDispatch the way to instruct the compiler to choose the right one.
  • Template enablement. This one consists to use std::enable_if and what is known as SFINAE (Substitution Failure Is Not An Error).
    template<typename T,
             typename = std::enable_if<
               !std::is_base_of<Person,std::decay<T>::type>::value
               &&
               !std::is_integral<std::remove_reference<T>::type>::value
               >::type
            >
    

Definition of SFINAE

If an invalid argument or return type is formed during the instantiation of a function template, the instantiation is removed from the overload resolution set instead of causing a compilation error.

What is considered an invalid argument or an invalid return type? - A function with no return type specification is invalid. All functions must define a return type.

With respect to function templates, enable_if can be used in multiple different ways:

  • As the return type of an instantiatied function;
  • As an extra parameter of an instantiated function;
  • As an extra template parameter (useful only in a compiler that supports C++0x default arguments for function template parameters, see Enabling function templates in C++0x for details.

Implementation of enable_if in boost

template <bool B, class T = void>
struct enable_if_c {
    typedef T type;
};

template <class T>
struct enable_if_c<false, T> {};

template <class Cond, class T = void>
struct enable_if : public enable_if_c<Cond::value, T> {};

Utility template function

  • The types int and int& are not the same! Think of using std::remove_reference.
  • std::enable_if, std::is_base_of, std::is_constructible, static assert, std::decay

Item 28: Understand reference collapsing

  • You are not allowed to declare reference to reference variables. That is, the following code is forbidden.
int x;  
auto& & rx = x;
  • Nonetheless, the compiler is allowed to do so. When the character of a parameter passed to a universal reference parameter is a lvalue, the deduced type is T&. In other words, we have
//the function template
template<typename T>
void myFunction(T&&){ }

//generates
void myFunction(Widget& &&){ }

//when there is one invokation passing a lvalue reference
Widget w;
myFunction(w);

In these cases, the compiler generates a reference to reference and just after executes a reference collapsing.

  • Rule for reference collapsing: If either reference is an lvalue reference, the result is an lvalue reference. Otherwise (i.e., if both are rvalue references) the result is an rvalue reference.

Wherever there is a Universal reference, the following will happen:

  1. Lvalues of type T are deduced to have type T&, while rvalues of type T yield T as their deduced type;
  2. Reference collapsing occurs.
auto&& w1 = w;
//is deduced as
Widget& && w1 = w;
//after reference collapsing
Widget& w1 = w;

//On the other hand 
auto&& w1 = widgetFactory();
//is deduced as
Widget&& w1 = widgetFactorty;
  • Reference collapsing also happens during typedefs
template<typename T>
class Widget {
public:
typedef T&& RvalueRefToT;
...
};

Widget<int&> w;

//is deduced as
typedef int& && RvalueRefToT;

//reference collapsing occurs and thus
typedef int& RvalueRefToT; //We have a lvalue instead of rvalue.
  • Besides template, auto and typedef instantiation, reference collapsing also occurs in decltype.

Reference collapsing is in the realm of why std::forward works, that is, why std::forward returns lvalue if the template character is a lvalue or a rvalue if the character is a rvalue. Here is the implementation of std::forward

template<typename T>
T&& forward(typename remove_reference<T>::type& param){
    return static_cast<T&&>(param);
}

Here it is an example using universal references and forward:

template<typename T>
void f(T&& x){
    return my_func(forward<T>(x));
}

CPP-Conf-20: C++20 Ranges In Practice

std::ranges::dangling protection

//till C++17, this would return a dangling iterator
//in C++20, it returns a std::range::dangling. It is basically
//a empty struct, but the compiler now shout out an error if we
//try to effectuate an operation on it. Good!
auto iter = std::ranges::min_element(get_vector());

The std::ranges::dangling is returned every time a rvalue is passed to a function of the ranges library. However, some types are safe to pass by rvalue. For example, string_view and span. We can disable this behaviour for our specific type by specializing enabled_borrowed_range.

template<>
inline constexpr bool
std::ranges::enable_borrowed_range<my::StringRef> = true

Views

A view is a range which is - default constructible - has constant-time move and destruction operations - (if it is copyable) has contant-time copy operations

//views are lazy sometimes
auto view = std::views::transform(vec,[](int i){ return i*i; })
auto sumsq = std::accumulate(view.begin(),view.end(),0);
  • What is std::ranges::view::common?

std::invoke

It calls a callable object with a list of arguments. The callable object could be a function object, a pointer to member function or a pointer to data member.

  • pointer to member function: INVOKE(f, t1, t2, ..., tN) is equivalent to (t1.*f)(t2, ..., tN)
  • pointer to data member: INVOKE(f, t1) is equivalent to t1.*f
  • function object: INVOKE(f, t1, t2, ..., tN) is equivalent to f(t1, t2, ..., tN)

std::projection

Views and ranges adapter

  • std::views::drop_while
  • std::views::reverse
  • Adapters composition to create new adapters

Examples

template<typename R>
auto trim_back(R&& rng){
    return std::forward<R>(rng)
        | views::reverse
        | views::drop_while(::isspace)
        | views::reverse;
}
auto trim_back(){
    return views::reverse
        | views::drop_while(::isspace)
        | views::reverse;
}

auto trim_front(){
    return views::drop_while(::isspace);
}

//I can transform this in an inline constexpr variable! Same for the two above
auto trim(){
    return trim_front() | trim_back();
}

std::string trim_str(const std::string& str){
    return string_view(str) 
        | trim()
        | rangesnext::to<std::string>; //external library
}

Item 29: Assume that move operations are not present, not cheap, and not used

Scenarios

  • No move operation.
  • Move not faster than copy. Moving an array is tantamount copying an array. Or copy for smal strings (Small String Optimization, SSO). String with fewer than 16 characters are stored in an internal buffer and there is no memory allocation involved.
  • Move not usable. If not declared as noexcept and the function supposed to use the move semantics imposes strong exception safety guarantee.
  • Source object is lvalue. In some situations, it is ok to move lvalues. For example, copy-by-value function parameters which all modifying operations have been done prior to the move operation. But in general, move operations are supposed to be done in rvalues.

In my opinion, these reasons state the opposite, actually. They say that move semantics may not be as efficient as expected, but in these cases they are going to be as efficient as copy operations. Therefore, it is better to favor move operations because in the worst case, you are going to get the runtime of a copy, which is the alternative to move operations.

Item 30: Familiarize yourself with perfect forwarding failure cases

Braced initializers

Consider the following scenarios:

void f(std::vector<int> v){ ... }

template<typename T>
void fwd(T&& v){
    f(std::forward<T>(v));
}

f({1,2,3});     //Call 1
fwd({1,2,3});   //Call 2

In the first call, the compiler 'sees' that the type should be deduced to std::vector<int> and implicit conversion applies. But in the second, the compiler has no prior information about the type to be deduced at the call site, that is, fwd({1,2,3}).

According to the standard, std::initializer_list is a "non-deduced context". That means that compilers are forbidden from deducing a type for the expression {1,2,3} in the call to fwd.

Workaround:

auto il = {1,2,3};  //For auto, compilers are allowed to deduce this as being an `std::initializer_list`
fwd(il); //That works

0 or NULL

This is clear. The way to go is to use nullptr.

Declaration-only integral static const data members

Generally, you don't need to define integral static const data member. In most situations you are set by simply declaring it.

class C{
    static const int VAL = 42;
}

As long as you don't create a pointer to VAL, everything will work fine. The compile will plop the value 42 every time it finds C::VAL. But if you work somehow with a pointer to VAL then an error will occur, because in this case you will need a definition.

It happens that when forwarding integral static const members we have the same problem, and we need to define the integral member.

//in a cpp file
const int C::VAL;

Overloaded function names and template names

Consider this

int processFunction(int a);
int processFuntion(int a,int b);

void f(int (*pf)(int));

template<typename T>
void fwd(T&& v){
    f(std::forward<T>(v));
}

f(processFunction);     //call 1
fwd(processFunction);   //call 2

In the first call, everything goes fine. The compiler chooses the correct overload of processFunction. In call 2, by a reason similar to the first topic in this item, the compiler does not know which overloaded function to pass at the call site of call 2.

Once more, the workaround is to define a variable taking the type of the correct overload function and then pass this variable to fwd.

Bitfields

We declare bitfields in C++ as the following

struct B{
    int a:2,b:4,c:6,d:2;
    //2 unused bits, assuming int occupies 2 bytes.
};

Because bitfields do not necessarily begin at the beginning of a byte, address of a bitfield can't be taken. Pointers and non-const references to bitfields are not possible. When initializing a const reference from a bitfield, a temporary is created (its type is the type of the bitfield), copy initialized with the value of the bitfield, and the reference is bound to that temporary.

The reason why bitfields cannot be forwarded lies in the fact that we cannot have pointers to them (remember that, internally, pointers and references are similar). We need first to bound the bitfield to a variable in order to forward it.

B h;
auto length = static_cast<std::uint16_t>(h.c);
fwd(length);

Item 31: Avoid default capture modes

There are two default capture modes. - By reference: [&] - By value: [=]

These modes translate to: Make available in the lamba context, everything that is available in the context where the lambda was created.

The main precaution to be take here is to ensure that the lifetime of the captured values outlives the lifetime of the lambda function. Having saying that, it is a good practice to be explicit about which variables the lambda context will capture instead of using default modes. The rational is that by expliciting listing the variables, we are more likely to remember that those must outlive the lambda.

One could think that the problem described above it is only sensible to [&]. That is, that the problem occurs only if the captured variable is destroyed before a call to the lambda (which would create a dangling reference to the captured variable). But we may face a similar issue using the by value default mode [=].

std::vector< bool(int*) > filters;
class Widget{
public:
  void addFilter() const;
private:
  int divisor;
};

void Widget::addFilter() const{
  filters.emplace_back(
    [=](int value){ return value % divisor == 0; }
  );
}
Although the filter lambda function is using the default capture all by value mode, nothing is being captured there. Lambda contexts are only allowed to capture non-static local variables. Here, divisor is a member of Widget and not a local variable in the context of addFilter.

Despite that, this code compiles. That's because the variable this is implicitly captured, and divisor is implicitly substituted to this->divisor. As you know, this is a pointer, and a copy by value won't save you from dangling country if the lambda function outlives the Widget which created it.

Here it is another example:

void addDivisorFilter(){
    static auto calc1 = computeSomeValue1();
    static auto calc2 = computeSomeValue2();

    static auto divisor = computeDivisor(calc1,calc2);

    filters.emplace_back(
      [=](int value){ return value % divisor == 0; }
    );

    ++divisor;
}

Once again, nothing is being captured here. Lambda contexts do not capture static variables. The code above compiles because it is referring directly to the stati divisor variable. In fact, it is like divisor is being passed by reference. We have no clusure at all in this case as well. Every time we invoke the created lambda, the value of divisor may had changed.

Item 32: Use init capture to move objects into closures

In C++14, the C++11 capture mechanism is super seeded by what is called the generalized capture. Here it is an example

auto pw = std::make_unique_ptr<Widget>();

//...configure pw

[pw = std::move(pw)](){
    // do some stuff with pw
}()
The generalized capture is what is in between brackets. Notice that in the left of = we are in the scope of the lambda class (the class that every lambda generates to encapsulate the capture object); at the right of = we are in the scope where the lambda was created.

In C++11, it is not possible to move-construct an object into a closure, as it is possible with the generalized capture mechanism in C++14, but there is a workaround to it.

auto func = std::bind(
  [](const std::vector<double>& data){ ... },
  std::move(data)
);

Cpp-Conf-20: OO Considered Harmful

References: 1. GO TO Statement considered harmful. Dijkstra.
There is an argument about how our mind poorly perceives time. Go To would interfer in a linear structure of a program. 2. The Forgotten Art of Structured Programming. 3. Squaring the circle.

Brief story of OO. Language Hierarchy

             - SmallTalk

Algol - Simula / - Objective C - C -\ - C++

Message passing (dynamic binding) x Virtual dispatch in C++ (Virtual tables)

What are the costs of Virtual Tables? (Heap allocation?)

What is Type Erasure? (e.g. Dyno)

Look at std::visit

Polymorphism: It is really necessary? Single inheritance isn't enough? Sure we can have the same effect with single inheritance, but polymorphism simplify stuff. Encapsulation: Do we need OO to have encapsulation (think abount const public members)

Cpp-Conf-20: C++20: An almost complete overview

References: Professional C++. Marc Gregoire

  • Modules
  • Ranges
  • Coroutines
  • Concepts

Modules

  • Will replace header files in the long run.
  • Code declared in the interface (header) won't be exported. That means that a change in the interface body won't trigger a rebuild everywhere the interface is needed.
  • No need for include guards
  • Example of a module:

// Create a Module
// ccpcon.cppm - Module Interface File
export module cppcon; // Module declaration

namespace CppCon{
    auto GetWelcomeHelpert() { return "Welcome to CppCon 20020!"; }
    export auto GetWelcome() { return GetWelComeHelper(); }
}

// Consume a Module
// main.cpp
import cppcon;

int main(){
    std::cout << CppCon::GetWelcome();
}
I can use import <cstdio>. It will export everything in the interface. When importing a C++ header file, also the macros are exported. Normally, macros are not exported.

Ranges

How it works the lazy characteristic of Ranges?

Range: A concept defining iteration requirements. Any container supporting begin/end is a valid range.

Range-based algorithms: All STL algorithms accepting ranges instead of iterator pairs.

Projection: Transform elements before handing over to algorithm.

Views: Transform/filter range: lazily evaluates, non-owning, non-mutating

Range factories: construct views to produce values on demand

Pipelining: Views can be chained using pipes, e.g |

What is the difference between a view and a projection?

Example with infinite sequences:

auto result { view::ints(10) //infinite list of integers starting at 10
  | views::filter([](const auto& value) { return value % 2 == 0; })
  /* ... */
  | views::take(10) //Takes first 10
};

Coroutines

Those that have co_yield, co_await.

It is useful to create generators and do lazy computation.

Concepts

// Concept definition
template<typename T>
concept Incrementable = requires(T x){ x++; ++x; }

// Using the concept
template<Incrementable T>
void Foo(T t);

template<typename T> requires Incrementable<T>
void Foo(T t);

template<typename T>
void Foo(T t) requires Incrementable<T>;

void Foo(Incrementable auto t);

// Another example
template<typename T>
concept C = requires (T& x, T& y) {
    { x.swap(y) } noexcept;
    { x.size() } -> std::convertible_to<std::size_t>;
    ...
};

Lambda expressions

  • Default by value capture does not implicitly includes this anymore.
  • We now have template lambda expressions.
[]<typename T>(const vector<T>& vec){
  T x { };
  T::static_function();
  //...
};

constexpr

  • virtual functions can be constexpr.
  • dynamic_cast and typeid is also allowed
  • heap allocation is also allowed
  • string and vector are now constexpr!!!!

atomic smart pointers

  • we have a type atomic_shared_ptr

jthread

  • it automatically cancels thread and calls join() if the jthread object is destroyed before join or detach been called.

synchronization primitives

  • we have a semaphore class. It generalizes mutexes.
  • Other synchronization primitives: Latches & barriers.

misc

  • if and switch initialization
switch( auto data; data.value ){ }
  • floating-point types and classes (some restrictions) are accepted as non-type template parameters.

  • Checkout the CTRE library: compile-time string literals to create regular expressions parsers at compile time!!!

auto m { ctre::match<"[a-z]+([0-9]+)">(str) };

span

  • read-only span: std::span

using enum directive

switch(cardTypeSuit){
    using enum CardTypeSuit;
    case Clubs: ...
    case Diamonds: ...

std::format

It combines the best of stream operators and printf. There is no reason no to use it

Represents information about a specific location in a source code: line, column, filename, function name

Item 33: Use decltype on auto&& parameters to std::forward them.

In C++14 we have generic lambdas with the auto keyword. It will be clear in the following that this is different from template lambdas that debuted in C++20.

[](auto x){ func(normalize(x)); }

But what about if the parameter x that we pass to our lambda could be a left or a right value? We should use a universal reference

[](auto&& x){ func(normalize<std::forward<???>(x)); }

We don't have access to the deduced type by the auto keyword, so we are not able to pass the template parameter to the forward function, and we must pass it (the reason for that is in how forward work. An explanation is given in item 28). If we rewrite it as

[](auto&& x){ func(normalize<decltype(x)>(x)); }

Then decltype resolves to T& if x is a lvalue and resolves to T&& if x is a rvalue. If we look at the definition of std::forward

template<typename T>
T&& forward(remove-reference_t<T>& param){
    return static_cast<T&&>(param);
}

and we apply the reference collapsing rules, we are going to obtain exactly what we want, despites the convention of passing T& when forwarding a lvalue and T when forwarding a rvalue. Passing T&& as the template parameter for the forward function also resolver to casting the parameter to a rvalue.

Item 34: Prefer lambdas to std::bind

auto setSoundL = [](Sound s){
    using namespace std::chrono;

    setAlarm(steady_clock::now() + &h,
             s,
             30s);
};

auto setSoundB = std::bind(static_cast<SetAlarm3ParamType(setAlarm),
                           std::bind(std::plus<>(),
                                     steady_clock::now(),
                                     1h),
                           _1,
                           30s);

std::bind uses function pointers to invoke the binded function. Compilers have more difficult to inline functions invoked by function pointers. In these situations, lambdas are also faster.