A summary of modern C++ features

A beautiful scene

This blog post summarizes the main features of modern C++ from the developer’s point of view.

Inheritance

A derived class can inherit protected and public members of the base class. This creates an is a relationship, so the derived class’ objects are also of type base class. And hence, a pointer of type base class can point to an object of the derived class.

A class can inherit from another class privately, protectedly, or publicly. With public inheritance, public members of the base class become public members of the derived class, protected members of the base class also become protected members of the derived class. With protected inheritance, public and protected members of the base class all become protected members of the derived class. With private inheritance, public and protected members of the base class all become private members of the derived class. (See table below.)

C++ Inhertance modes

Base class memberInheritance mode
PublicProtectedPrivate
PublicPublicProtectedPrivate
ProtectedProtectedProtectedPrivate
Privatenot inheritablenot inheritablenot inheritable

Note that as they are special, the destructor and friends are not inheritable. The constructor is also not inherited by default, however, from C++11, we can inherit all constructors using the keyword using.

When an object of the derived class is initialized, the constructor of the base class (we can choose which one) will be called before the constructor of the derived class. On the reverse, when an object is deleted, the derived class’s destructor will be called before it of the base class.

Multiple inheritance is when one class derives from multiple base classes. Hierarchical inheritance is when more than one class is derived from one base class. The combination of Multiple inheritance and Hierarchical inheritance (or any other 2 types of inheritance, like Single inheritance and Multilevel inheritance) is called Hybrid inheritance. Typically, these types of complex inheritance are not encouraged as they affect the comprehensibility of the program and are prone to errors.

Access specifiers: private, protected, public

These are the 3 access modes of classes and structs.

  • A private member (i.e. attribute or method) cannot be accessed from outside of the class and is not inheritable by subclasses. It can only be accessed from inside of the class.
  • A protected member cannot be accessed from outside of the class but is inherited by subclasses.
  • A public member can be accessed outside of the class and is also inherited.
#include <iostream>
#include <string>

class Base
{
private:
    std::string private_attr;

protected:
    std::string protected_attr;

public:
    std::string public_attr;
};

class Derived : public Base
{
public:
    Derived()
    {
        protected_attr = "this is accessible from the inside of derived classes";
    }
};

int main()
{
    Derived variable;
    variable.public_attr = "this is even accessible from anywhere";
}

Struct vs Class

In C++, structs and classes are almost exactly the same. The only difference lies in their default access mode:

  • Members of structs are public by default. Members of classes are private by default.
  • When deriving a struct from a struct (or class), if not specified, the default inheritance mode is public, while this is private for a class.

Overloading vs Overriding

Overloading means we define multiple functions (and/or operators) with the same name but different parameter types. Overriding means when deriving a new class, we re-define the same function (i.e. same name and same parameter types) that had been defined in the base class.

Overloading functions might have the same or different return types, but they must have different parameter types. In other words, we cannot overload functions distinguished by return type alone.

In general, the compiler considers overloading functions as separate functions.

#include <iostream>

class Base
{
public:
    int foo(int x)
    {
        return x + 1;
    }

    float foo(int x, float y) // overloading of `int foo(int x)`
    {
        return x + y;
    }
};

class Derived : public Base
{
    float foo(int x, float y) // overriding of `float foo(int x, float y)` in the base class
    {
        return x - y;
    }
};

int main()
{
}

Static binding and Dynamic binding

Binding means mapping from each function call to the actual function to be triggered. Static binding (or early binding) happens at compile-time while dynamic binding (or late binding) happens at runtime.

Static binding happens with normal function calls and overloaded functions/operators. Dynamic binding can happen with virtual functions.

While static binding makes the program faster, dynamic binding is more flexible since one function call can behave differently depending on the object calling it.

Virtual function

Within the scope of inheritance, a virtual function (defined by the virtual keyword) of the base class signals dynamic binding, i.e. the call to the function name will trigger which function, the one defined in the base class or the one defined in any subclass, is determined at runtime.

If in the base class, the body of the virtual function is not implemented, that function is called a pure virtual function. Any class that has a pure virtual function is a (virtual) abstract base class. Concrete subclasses do not need to implement all virtual functions of the base class, but only the pure ones.

When overriding a pure virtual function, while it is not a must, it is always recommended to add the override keyword:

  • It tells the reader that this function overrides a pure virtual function of the base class.
  • The compiler can verify if this is truely an overriding of a pure virtual function, and notice the programmer by giving an error if this is not.
#include <iostream>

class Base
{
public:
    virtual int foo(int x) = 0; // a pure virtual function

    float foo(int x, float y) // a pure virtual function can also be overloaded
    {
        return x + y;
    }
};

class Derived : public Base
{
    int foo(int x) override // we implement all pure virtual functions of the base class
    {
        return x + 1;
    }
};

int main()
{
    Derived var; // since we implement all pure virtual functions of Derived's parents, we can instantiate objects from Derived
}

Polymorphism

Polymorphism means many forms, or the same entity acts differently in different scenarios.

There are 2 types of polymorphism: in compile-time (i.e. static binding, early binding) and in runtime (i.e. dynamic binding, late binding). Compile-time polymorphism includes overloading of functions and operators, while runtime polymorphism refers to overriding of pure virtual functions.

Data abstraction

Data abstraction is the ability to hide minor details and only show to users the essential information and functions.

In C++, data abstraction can be achieved by:

  • The access specifiers: e.g. the private and protected members of a class are not to be accessed from the outside world.
  • The header files: detailed implementation of the functions are hidden.
  • Abstract base classes and pure virtual functions.

friend

A class can have friends that are other classes, functions, class templates, and function templates. They are marked with the keyword friend. while class A considers X as a friend, X is not a member of A, however, X can access all private and protected members of A.

The relationship is not symmetric, that is, while A considers X a friend, X does not necessarily consider A a friend. Furthermore, a friend function can be a global function or a member of another class.

Friendship declaration can be anywhere in the body of a class (no matter in the private, protected, or public region).

Friendships are not inheritable.

Normally, we use friend functions to access private and protected members of more than 1 class. See the code below for illustration.

#include <iostream>

class B; // forward declaration

class A
{
private:
    int private_attr;

public:
    A(int v)
    {
        private_attr = v;
    }
    friend float a_friend(A, B); // friendship declaration
};

class B
{
protected:
    float protected_attr;

public:
    B(float v)
    {
        protected_attr = v;
    }
    friend float a_friend(A, B); // friendship declaration
};

float a_friend(A a, B b) // this function is a friend of both A and B, so it can access private and protected members of these 2 classes.
{
    return a.private_attr + b.protected_attr;
}

int main()
{
    A a(12);
    B b(3.5);
    std::cout << a_friend(a, b) << std::endl; // output: 15.5
}

Pointer and reference

While most languages only support referencing, C++ has both pointers and references. Since they are very similar, it can be confusing when to use which.

Pointer and reference both involve an original object that provides access to another one. While the pointer is a separate variable that holds the memory address of this original object, the reference is an alias (can be considered as another name) of the object.

A pointer can be declared without initialization, while a reference needs to be initialized at declaration time. While a pointer can be reassigned to another object, a reference can’t. Only pointers can be assigned to nullptr, a reference has to be assigned to a real object.

While in general, both pointers and references can only refer to objects of a pre-defined type, a void pointer can point to any type. Note, however, that the reverse is not true unless we do type-cast.

Normally, it is recommended to use references. Pointers should only be used if the work cannot be done with references, for example when re-assignments are needed, or pointing to null is allowed. A real use-case of pointers could be to implement the linked-list data structure.

#include <iostream>

int main() 
{
    int x = 3;
    
    int &y = x; // y is a reference of x
    std::cout << y << std::endl; // output: 3
    y = 10; // changing y will also change x since both are the same
    std::cout << x << std::endl; // output: 10

    int *p = &x; // p is a pointer that points to x
    std::cout << *p << std::endl; // output: 10
    *p = 20; // change the `*p` (i.e. the value p points to) will also change x and y
    std::cout << x << std::endl; // output: 20

    int z = 200;
    *p = z; // this changes the value at which p points to
    p = &z; // this changes where p points to, p is a pointer so it can be re-assigned
    std::cout << *p << std::endl; // output: 200

    void *r = &x; // a void pointer can point to any type
}

Passing by value and reference

By default, things in C++ are passed by value.

Arrays are a special case. When we pass an array, we pass the pointer to the first element of that array (and so it is very similar to passing by reference, i.e. no overhead for copying, changing inside the function will reflect in the original array).

The symbol & makes passing by reference (e.g. void foo(int &x) {...}). About arrays, there is usually no benefit to passing an array by reference.

One special (but quite frequent) case to remember is with threads, as described here, in which we need std::ref to passing by reference.

The this pointer

Member methods of a class can use the this pointer, which is a pointer to the current object that is calling the corresponding method.

Friend classes and friend methods cannot use the this pointer, as they are not members of the class.

Template

A function (or class) template is a function (or class) that can operate on different input types. That said, if our function is meant to behave the same given either int or double input type, we use Template to write only one function that works for both types.

From another point of view, templates allow us to pass data types in a similar way to passing parameter values to a regular function.

#include <iostream>

template <class T>
T add(T a, T b)
{
    return a + b;
}

int main()
{
    int x = 2, y = 3;
    std::cout << add<int>(x, y) << std::endl; // output: 5

    double u = 7.1, v = 8.5;
    std::cout << add<double>(u, v) << std::endl; // output: 15.6
}

Block scope variable

Any variable that is declared inside a block is only accessible inside that block. Attempts to call a variable outside of its block scope will result in a compiler error. A block is any piece of code that is enclosed by a pair of curly brackets, like the if block, for block, etc.

Exception handling

Exception handling in C++ is done using the 3 keywords: try, catch, and throw.

Note the difference between using assert and exceptions: assert is meant to ensure the code is correct (and is meant to catch coding errors from the programmer) while exceptions are to catch what goes wrong during users’ use. There is no need to handle (in code) the errors caught by assert, because they are never meant to exist.

ternary operator

The ternary operator is a shorthand for an if-else block. It has the syntax:

variable = (BooleanCondition) ? ExpressionTrue : ExpressionFalse;

const and define

The const keyword can be used to mark a variable as unchangeable, i.e. being a constant. In some cases, it gives similar effects as #define.

#include <iostream>

#define defined_variable 200

const int const_variable = 100;

int main()
{
    std::cout << const_variable << " " << defined_variable << std::endl; // output: 100 200
    // const_variable = 10; // Error
    // defined_variable = 20; // Error
}

const for pointers

There are different ways we can use the const keyword for pointers.

1. A pointer to point to a const value: const <type> *p = &<constant variable>. A constant pointer defined this way can point to a regular (i.e. not const) value, however, it is recommended to use it only to point to a constant value. This declaration prevents users from modifying the value to which the pointer points.

#include <iostream>

int main() 
{
    int a = 1, b = 2;
    const int c = 3;

    const int *p; // This prevents changing the value p points to by assigning *p = ...
    int *q;
    p = &a; // This is allowed
    // *p = 10; // Error, because const pointer p is not allowed to change the value it points to
    p = &b; // Good, p is allowed to be re-assigned
    // q = &c; // Error, a pointer of type `int *` cannot point to an object of type `const int *`
    p = &c; // This is recommended.

    std::cout << *p << std:: endl;  //output: 3
}

Note that as illustrated in the code above, a const pointer can point to a non-const value, however, the reverse is not true: a non-const pointer cannot point to a const value.

2. When we want to ensure a pointer doesn’t change its initialized pointing address, i.e. the pointer can point to 1 address its entire lifetime: <type> *const p = &<variable>.

#include <iostream>

int main()
{
    int a = 1, b = 2;
    const int c = 3;

    int *const r = &a; // This prevents changing the address that r points to. r can only point to 1 address for its entire lifetime.
    // r = &b; // Error, cannot change r's pointing address.
    *r = 4; // This is allowed

    std::cout << *r << std::endl;
}

3. Just a combination of the above 2 cases, i.e. a pointer that always points to a pre-defined constant value: const <type> *const p = &<variable>.

#include <iostream>

int main()
{
    int a = 1, b = 2;
    const int c = 3;

    const int *const s = &c;
    // s = &a; // Error
    // *s = 5; // Error

    std::cout << *s << std::endl;
}

An easy way to remember what is made constant by const is: const always affects the thing on its immediate left, if there is nothing on its left, then it affects the thing on its immediate right.

const for classes

A const attribute of a class needs to be initialized in the constructor, as below.

#include <iostream>

class A
{
public:
    int attr;
    const int const_attr;

    A(int c) : const_attr(c) {}
};

int main() {
    A a(100);
}

A const method (e.g. int foo() const {...}) is not allowed to change any member variables. If an object is declared as const (e.g. const Car car), it can only call const members of the class. This is to prevent any const object from having its internal attributes changed.

#include <iostream>

class A
{
public:
    int attr;
    const int const_attr;

    A(int c) : const_attr(c) {}

    int foo() const // this is a const function, so it is not allowed to alter any member attributes
    {
        return 10;
    }

    int bar()
    {
        return 20;
    }
};

int main()
{
    A a(100);
    a.foo();
    a.bar();

    const A b(200);
    b.foo();
    // b.bar(); // Error, const object cannot call non-const members
}

Note that a const function has the const keyword at the end, as in the code above. A function with const at the beginning, like const int func() {...} simply means its return type is const int.

static

The static keyword has several uses in C++. The overall idea is that if an element (i.e. object, attribute, method) is static then it is only initialized once.

1. static objects: the use of static objects is best described by an object that is initialized inside a function. Even though we call that function many times, that object is only initialized during the first call.

#include <iostream>

// this function shows how a static variable behaves inside a function
void foo()
{
    static int counter = 0;
    std::cout << counter++ << std::endl;
}

int main()
{
    for (int i = 0; i < 3; ++i)
    {
        foo();
    } // output: 0 1 2
}

2. static attributes: a static attribute of a class is only initialized once and used for all class objects. Static attributes can be either defined inside the class (using inline, from C++17) or outside the class. The old rule to only initialize static attributes outside of the class was to ensure initialization happens only once even though the header files can be included in many source files. (However, this is then nicely handled with C++17.)

#include <iostream>

class A
{
public:
    inline static double s_1 = 2.5; // this shows how to initialize static attributes inside class definition
    static float s_2;               // this static attribute is not defined inside class
};

float A::s_2 = 6.6; // this shows how to initialize static attributes outside class definition

int main()
{
    A::s_2 = 3.1;                                      // static attributes can be changed like this
    std::cout << A::s_1 << " " << A::s_2 << std::endl; // output: 2.5 3.1
}

3. static methods: static methods are also initialized once and used by all objects of the class. Note that static methods can access and alter only static attributes (but no non-static attributes).

#include <iostream>

class A
{
public:
    inline static double s_1 = 2.5; // this shows how to initialize static attributes inside class definition
    static float s_2;               // this static attribute is not defined inside class

    static void bar()
    {
        std::cout << s_1 << std::endl;
    }
};

float A::s_2 = 6.6; // this shows how to initialize static attributes outside class definition

int main()
{
    A::s_2 = 3.1;                                      // static attributes can be changed like this
    std::cout << A::s_1 << " " << A::s_2 << std::endl; // output: 2.5 3.1

    A a;
    // This is one way to call a static method
    A::bar(); // output: 2.5
    // This is another way to call a static method
    a.bar(); // output: 2.5
}

inline

Traditionally, inline functions are provided by C++ to replace macros (i.e. #define). By marking a function as inline, we hint the compiler to expand the body of the function inline where the function is called in order to optimize for performance. However, as modern compilers are smart enough to make the decision themselves, using inline in this way yields no more advantages.

At the moment, it is recommended to use inline almost only when it is strictly required. One of those cases is when we initialize static attributes inside the class definition. Another is to define the full body of a function in a header (.h) file.

By the way, it is important to note that all methods that are specified inside their class definition are implicitly inline. Thus, it is advised to define large methods outside of their class definition using the scope resolution :: operator.

#include <iostream>

class A
{
    inline static int x = 1; // initializing static attributes is one use-case of inline
public:
    void short_method()
    {
        // short methods can be defined here and be automatically inline
    }
    void large_method(double, int);
};

void A::large_method(double u, int v)
{
    // This is a large method.
    // Large methods like this should be defined outside of the class definition.
}

int main()
{
    A a;
}

volatile

The volatile type qualifier is often used in multi-threading or multi-processing programs. A variable is marked as volatile (e.g. volatile int x) if its value can be changed by another thread (or process). Thus, the compiler must not assume that the variable’s value is unchanged even if the current thread (or process) doesn’t touch it. And hence, the compiler needs to evaluate its value every time the variable is called.

extern

extern is a storage class to indicate that the variable, or function is declared in an external scope, for example, in another .cpp file. For a variable or function to be used in other places from where it is declared, we:

  • Declare and possibly initialize it normally (e.g. int x = 1).
  • At other scopes where we need to use it, declare that we want to have an external reference to it (e.g. extern int x) before using it as a regular variable or function.

Take a look at the example below for illustration.

main.cpp:

#include <iostream>
int v;
extern void foo();

int main()
{
    v = 100;
    foo();
}

support.cpp:

#include <iostream>

extern int v;

void foo(void)
{
    std::cout << v << std::endl;
}

terminal:

$ g++ main.cpp support.cpp -o main
$ ./main

output:

100

namespace

A namespace is a declarative region to organize code into different groups to prevent name collision. A namespace is declared using the following syntax:

namespace Space
{
    int x = 5; // variables...
    void foo() { } // functions...
}

Elements of a namespace can be accessed from outside using the scope resolution ::, e.g. std::cout << Space::x;. Elements of the global scope can always be accessed with nothing before ::, e.g. ::foo() (we do this if there exists a local element with the same name).

To use a namespace’s elements directly without the need to specify their namespace, we use the directive using, like using namespace std;.

Namespaces can even be nested, in which the child namespace has free access to the parent namespace’s elements.

namespace Space
{
    void foo() { cout << "foo" << endl; }

    namespace ChildSpace
    {
        void bar() { foo(); }
    }
}

storage classes

A storage class describes some features (the scope, visibility, and lifetime) as variables and functions. There are 5 storage classes:

  • auto: this is the default storage class. If your variables and functions are not specified with any of the other storage class, the compiler implicitly understands them as auto.
  • static: this controls lifetime of variables and functions, as described in this section.
  • extern: this controls the visibility, as described in this section.
  • register: this suggests the compiler to store the corresponding variables in computer registers if possible for faster access.
  • mutable: this is used on a class attribute to make it modifiable by any const object.

constexpr

The constexpr (abbreviated for constant expression) keyword behaves quite similarly to const. However, constexpr can be considered more strict than const in the sense that its value (for variables) or return value (for functions) needs to be computable at compile time. If a variable is constexpr, it is also const.

Stack and heap memory

The stack memory is used to allocate local variables. The structure of a stack is suitable for entering and exiting scopes: always, the current scope’s variables are at the head of the stack, which makes it faster to access.

The heap memory, on the other side, stores global variables and dynamically allocated data. Global variables are active during the whole program’s lifetime, they are only deleted when the program finishes, so pushing them into a heap would result in more efficient access. Any dynamic allocation of memory is performed on the heap because the heap memory has no limit on size and variable resizing is only possible in the heap (and not possible in the stack).

Here we compare the advantages of stack and heap memory:

C++ advantages of stack and heap memory

StackHeap
Access timeFasterSlower
DeallocationAutomaticallyNeed to be handled by the programmer
SizelimitedBasically no limit
Re-sizeNot possiblePossible

The below code shows examples of when variables are stored on stack and heap:

int x;              // Heap
double y[10000000]; // Heap

void foo()
{
    float u = 3;
}

int main()
{
    int a;                  // Stack
    foo();                  // Stack
    double *p = new double; // Heap
    int *q = new int[10];   // heap

    // double s[10000000]; // Runtime error, as stack cannot store this big size
}

Shallow and deep copy

Shallow and deep copy are different if the dynamic allocation of memory is used. In this case, with shallow copy, only the reference is copied, i.e. both objects point to the same location in memory and changes made by one object will also affect the other. With deep copy, a new memory address is created for the new object so the 2 objects are by all means separated.

Shallow copy is the default for the copy constructor and assignment operator (=). Thus, while it works perfectly if the object’s attributes are simple, re-implementation of the copy constructor and assignment operator are needed to make them using deep copy. The problem is illustrated in the below code.

#include <iostream>

class ShallowClass
{
public:
    int *p;
    ShallowClass(int p_) : p(&p_) {}
};

int main()
{
    ShallowClass a(1);
    std::cout << *a.p << std::endl; // output: 1

    ShallowClass b = ShallowClass(a); // this invokes the default copy constructor
    *b.p = 2;                         // change the value of the b object, which also affects in the value of the a object.

    std::cout << *a.p << std::endl; // output: 2
}

explicit

The explicit keyword prevents constructors or conversion functions from doing implicit cast. In the below example, on line 16, A c = 1; implicitly casts the int 1 to A type using the constructor A(int) before assigning it to the left-hand side. The same cannot happen on line 17 as A(std::string) is declared as explicit.

#include <iostream>
#include <string>

class A
{
public:
    A(int) {}
    explicit A(std::string) {}
    A(int, double) {}
};

int main()
{
    A a(1);     // Ok. This calls A(int)
    A b("abc"); // Ok. This calls A(std::string)
    A c = 1;    // Ok. This (implicitly) calls A(int)
    // A d = "abc"; // Error. This doesn't implicity call A(std::string)
    A e = (A) "abc";             // Ok. This explicitly casts "abc" to A type before assigning it to e.
    A f = static_cast<A>("abc"); // Ok. This also cast "abc" to A type.
}

Type inference

Since C++11, we do not need to specify the type of variables everywhere but rather can tell the compiler to make induction on variable types in some places. auto and decltype are our helpers. The auto keyword commands the compiler to trace back until the declaration of the variable to see its type. On the other hand, decltype returns the type of a variable, function, or expression.

#include <iostream>
#include <vector>

std::string foo()
{
    return "foo";
}

int main()
{
    auto x = 1.2; // auto: the compiler can infer the type of x is double because 1.2 is a double

    std::vector<int> vec;
    for (auto v : vec) {} // auto: the compiler can infer the type of v is int

    decltype(foo) y; // decltype: declare y as of type std::string
}

std::move

The std::move function converts an lvalue variable into xvalue. That is, std::move(x) says: if x‘s value needs to be copied, don’t copy, just take its value away. The advantage of doing this is to reduce the number of (costly) copies, and thus the program takes less time to run.

#include <iostream>

template <typename T>
void swap(T &a, T &b)
{
    T tmp = std::move(b); //  now tmp possesses the value of b without having to copy it.
    b = std::move(a);     // similar to above
    a = std::move(tmp);   // similar to above
}

int main()
{
    int x = 1, y = 2;
    swap(x, y);
    std::cout << x << " " << y << std::endl; // output: 2 1
}

Type alias

To make aliases for data types, we can use the typedef or using keyword. Examples below.

typedef long long ll;          // `ll` is now an alias for `long long`
using ul = unsigned long long; // `ul` is now an alias for `unsigned long long`

int main()
{
    ll x = 1;
    ul y = 2;
}

Dynamic memory allocation

In C, the canonical function for dynamic memory allocation is malloc (or calloc), deallocation by free, and reallocation by realloc. This changed when coming to C++, as now, allocation is done by the operator new and deallocation is done by delete and delete[]. If reallocation is needed, it is recommended to use standard library containers like vector instead.

The operator new can either allocate a value or an array of values, in this case, it returns the location of the first element in that array (note that array elements are sequential in memory). delete is used to deallocate a single value, while delete[] is meant to deallocate an array.

#include <vector>

int main()
{
    int *p = new int;           // Dynamically allocate memory, on heap
    double *q = new double[10]; // Dynamically allocate memory, on heap
    delete p;
    delete[] q;

    // If reallocation is needed
    std::vector<int> vec; // while vec's header info is stored in stack, its array of elements is on heap
    // We can let the vector grow naturally by pushing elements to it, or manually resize it, as below
    vec.resize(1000);
}

Multi-threading

From C++11, multi-threading is well supported in the standard library. As a wrap-up, we use std::thread to create (and by default, run) a thread to execute a function, and std::mutex (short for mutually exclusive) for locking shared resources.

Take a look at the below example:

#include <iostream>
#include <thread>
#include <mutex>

void accumulate(int &result, std::mutex &my_lock, int resource[], int lower, int upper)
{
    // Can do something here without shared resource
    {
        std::lock_guard<std::mutex> my_guard(my_lock); // my_lock is locked from here and will get unlocked when my_guard is out of scope, i.e. at the end of the current block scope
        for (int i = lower; i <= upper; ++i)
        {
            result += resource[i]; // changing `result` will not create data race because only 1 thread can change it at a time, thanks to the lock
        }
    }
    // Can do something here without shared resource
}

int main()
{
    int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

    int total_result = 0;
    std::mutex my_lock; // create a lock for shared resource
    std::thread t1(accumulate, std::ref(total_result), std::ref(my_lock), arr, 1, 3); // create and run thread t1
    std::thread t2(accumulate, std::ref(total_result), std::ref(my_lock), arr, 4, 7); // create and run thread t2

    t1.join(); // wait for t1 to complete
    t2.join(); // wait for t2 to complete

    std::cout << total_result << std::endl; // output: 28
}

Explanation:

  • To create and run a thread, we call std::thread‘s constructor, which accept the function to be executed as the first parameter, and then the parameters for this function.
  • As by default, all parameters passed to the function are passed by value, we need std::ref to pass their reference.
  • .join() method is to wait for the thread to complete by executing further code in the main process.
  • As total_result is a shared resource of all threads (i.e. all threads attempt to modify it), we need a lock for it to prevent data racing. The lock is created with std::mutex and monitored by std::lock_guard, as illustrated in the code. Here, we use a lock guard instead of doing .lock and .unlock manually to (1) prevent mistakes like forgetting to unlock, and (2) prevent cases when an exception is raised making the .unlock call not executed.

References:

One thought on “A summary of modern C++ features

Leave a Reply