Book Image

Modern C++ Programming Cookbook - Second Edition

By : Marius Bancila
5 (1)
Book Image

Modern C++ Programming Cookbook - Second Edition

5 (1)
By: Marius Bancila

Overview of this book

C++ has come a long way to be one of the most widely used general-purpose languages that is fast, efficient, and high-performance at its core. The updated second edition of Modern C++ Programming Cookbook addresses the latest features of C++20, such as modules, concepts, coroutines, and the many additions to the standard library, including ranges and text formatting. The book is organized in the form of practical recipes covering a wide range of problems faced by modern developers. The book also delves into the details of all the core concepts in modern C++ programming, such as functions and classes, iterators and algorithms, streams and the file system, threading and concurrency, smart pointers and move semantics, and many others. It goes into the performance aspects of programming in depth, teaching developers how to write fast and lean code with the help of best practices. Furthermore, the book explores useful patterns and delves into the implementation of many idioms, including pimpl, named parameter, and attorney-client, teaching techniques such as avoiding repetition with the factory pattern. There is also a chapter dedicated to unit testing, where you are introduced to three of the most widely used libraries for C++: Boost.Test, Google Test, and Catch2. By the end of the book, you will be able to effectively leverage the features and techniques of C++11/14/17/20 programming to enhance the performance, scalability, and efficiency of your applications.
Table of Contents (16 chapters)
13
Bibliography
14
Other Books You May Enjoy
15
Index

Understanding uniform initialization

Brace-initialization is a uniform method for initializing data in C++11. For this reason, it is also called uniform initialization. It is arguably one of the most important features from C++11 that developers should understand and use. It removes previous distinctions between initializing fundamental types, aggregate and non-aggregate types, and arrays and standard containers.

Getting ready

To continue with this recipe, you need to be familiar with direct initialization, which initializes an object from an explicit set of constructor arguments, and copy initialization, which initializes an object from another object. The following is a simple example of both types of initialization:

std::string s1("test");   // direct initialization
std::string s2 = "test";  // copy initialization

With these in mind, let's explore how to perform uniform initialization.

How to do it...

To uniformly initialize objects regardless of their type, use the brace-initialization form {}, which can be used for both direct initialization and copy initialization. When used with brace-initialization, these are called direct-list and copy-list-initialization:

T object {other};   // direct-list-initialization
T object = {other}; // copy-list-initialization

Examples of uniform initialization are as follows:

  • Standard containers:
    std::vector<int> v { 1, 2, 3 };
    std::map<int, std::string> m { {1, "one"}, { 2, "two" }};
    
  • Dynamically allocated arrays:
    int* arr2 = new int[3]{ 1, 2, 3 };
    
  • Arrays:
    int arr1[3] { 1, 2, 3 };
    
  • Built-in types:
    int i { 42 };
    double d { 1.2 };
    
  • User-defined types:
    class foo
    {
      int a_;
      double b_;
    public:
      foo():a_(0), b_(0) {}
      foo(int a, double b = 0.0):a_(a), b_(b) {}
    };
    foo f1{};
    foo f2{ 42, 1.2 };
    foo f3{ 42 };
    
  • User-defined POD types:
    struct bar { int a_; double b_;};
    bar b{ 42, 1.2 };
    

How it works...

Before C++11, objects required different types of initialization based on their type:

  • Fundamental types could be initialized using assignment:
    int a = 42;
    double b = 1.2;
    
  • Class objects could also be initialized using assignment from a single value if they had a conversion constructor (prior to C++11, a constructor with a single parameter was called a conversion constructor):
    class foo
    {
      int a_;
    public:
      foo(int a):a_(a) {}
    };
    foo f1 = 42;
    
  • Non-aggregate classes could be initialized with parentheses (the functional form) when arguments were provided and only without any parentheses when default initialization was performed (call to the default constructor). In the next example, foo is the structure defined in the How to do it... section:
    foo f1;           // default initialization
    foo f2(42, 1.2);
    foo f3(42);
    foo f4();         // function declaration
    
  • Aggregate and POD types could be initialized with brace-initialization. In the following example, bar is the structure defined in the How to do it... section:
    bar b = {42, 1.2};
    int a[] = {1, 2, 3, 4, 5};
    

A Plain Old Data (POD) type is a type that is both trivial (has special members that are compiler-provided or explicitly defaulted and occupy a contiguous memory area) and has a standard layout (a class that does not contain language features, such as virtual functions, which are incompatible with the C language, and all members have the same access control). The concept of POD types has been deprecated in C++20 in favor of trivial and standard layout types.

Apart from the different methods of initializing the data, there are also some limitations. For instance, the only way to initialize a standard container (apart from copy constructing) is to first declare an object and then insert elements into it; std::vector was an exception because it is possible to assign values from an array that can be initialized prior using aggregate initialization. On the other hand, however, dynamically allocated aggregates could not be initialized directly.

All the examples in the How to do it... section use direct initialization, but copy initialization is also possible with brace-initialization. These two forms, direct and copy initialization, may be equivalent in most cases, but copy initialization is less permissive because it does not consider explicit constructors in its implicit conversion sequence, which must produce an object directly from the initializer, whereas direct initialization expects an implicit conversion from the initializer to an argument of the constructor. Dynamically allocated arrays can only be initialized using direct initialization.

Of the classes shown in the preceding examples, foo is the one class that has both a default constructor and a constructor with parameters. To use the default constructor to perform default initialization, we need to use empty braces; that is, {}. To use the constructor with parameters, we need to provide the values for all the arguments in braces {}. Unlike non-aggregate types, where default initialization means invoking the default constructor, for aggregate types, default initialization means initializing with zeros.

Initialization of standard containers, such as the vector and the map, also shown previously, is possible because all standard containers have an additional constructor in C++11 that takes an argument of the type std::initializer_list<T>. This is basically a lightweight proxy over an array of elements of the type T const. These constructors then initialize the internal data from the values in the initializer list.

The way initialization using std::initializer_list works is as follows:

  • The compiler resolves the types of the elements in the initialization list (all the elements must have the same type).
  • The compiler creates an array with the elements in the initializer list.
  • The compiler creates an std::initializer_list<T> object to wrap the previously created array.
  • The std::initializer_list<T> object is passed as an argument to the constructor.

An initializer list always takes precedence over other constructors where brace-initialization is used. If such a constructor exists for a class, it will be called when brace-initialization is performed:

class foo
{
  int a_;
  int b_;
public:
  foo() :a_(0), b_(0) {}
  foo(int a, int b = 0) :a_(a), b_(b) {}
  foo(std::initializer_list<int> l) {}
};
foo f{ 1, 2 }; // calls constructor with initializer_list<int>

The precedence rule applies to any function, not just constructors. In the following example, two overloads of the same function exist. Calling the function with an initializer list resolves to a call to the overload with an std::initializer_list:

void func(int const a, int const b, int const c)
{
  std::cout << a << b << c << '\n';
}
void func(std::initializer_list<int> const list)
{
  for (auto const & e : list)
    std::cout << e << '\n';
}
func({ 1,2,3 }); // calls second overload

This, however, has the potential of leading to bugs. Let's take, for example, the std::vector type. Among the constructors of the vector, there is one that has a single argument, representing the initial number of elements to be allocated, and another one that has an std::initializer_list as an argument. If the intention is to create a vector with a preallocated size, using brace-initialization will not work as the constructor with the std::initializer_list will be the best overload to be called:

std::vector<int> v {5};

The preceding code does not create a vector with five elements, but a vector with one element with a value of 5. To be able to actually create a vector with five elements, initialization with the parentheses form must be used:

std::vector<int> v (5);

Another thing to note is that brace-initialization does not allow narrowing conversion. According to the C++ standard (refer to paragraph 8.5.4 of the standard), a narrowing conversion is an implicit conversion:

- From a floating-point type to an integer type.

- From long double to double or float, or from double to float, except where the source is a constant expression and the actual value after conversion is within the range of values that can be represented (even if it cannot be represented exactly).

- From an integer type or unscoped enumeration type to a floating-point type, except where the source is a constant expression and the actual value after conversion will fit into the target type and will produce the original value when converted to its original type.

- From an integer type or unscoped enumeration type to an integer type that cannot represent all the values of the original type, except where the source is a constant expression and the actual value after conversion will fit into the target type and will produce the original value when converted to its original type.

The following declarations trigger compiler errors because they require a narrowing conversion:

int i{ 1.2 };           // error
double d = 47 / 13;
float f1{ d };          // error

To fix this error, an explicit conversion must be done:

int i{ static_cast<int>(1.2) };
double d = 47 / 13;
float f1{ static_cast<float>(d) };

A brace-initialization list is not an expression and does not have a type. Therefore, decltype cannot be used on a brace-init-list, and template type deduction cannot deduce the type that matches a brace-init-list.

Let's consider one more example:

float f2{47/13};        // OK, f2=3

The preceding declaration is, however, correct because an implicit conversion from int to float exists. The expression 47/13 is first evaluated to integer value 3, which is then assigned to the variable f2 of the type float.

There's more...

The following example shows several examples of direct-list-initialization and copy-list-initialization. In C++11, the deduced type of all these expressions is std::initializer_list<int>:

auto a = {42};   // std::initializer_list<int>
auto b {42};     // std::initializer_list<int>
auto c = {4, 2}; // std::initializer_list<int>
auto d {4, 2};   // std::initializer_list<int>

C++17 has changed the rules for list initialization, differentiating between the direct- and copy-list-initialization. The new rules for type deduction are as follows:

  • For copy-list-initialization, auto deduction will deduce an std::initializer_list<T> if all the elements in the list have the same type, or be ill-formed.
  • For direct-list-initialization, auto deduction will deduce a T if the list has a single element, or be ill-formed if there is more than one element.

Based on these new rules, the previous examples would change as follows (the deduced type is mentioned in comments):

auto a = {42};   // std::initializer_list<int>
auto b {42};     // int
auto c = {4, 2}; // std::initializer_list<int>
auto d {4, 2};   // error, too many

In this case, a and c are deduced as std::initializer_list<int>, b is deduced as an int, and d, which uses direct initialization and has more than one value in the brace-init-list, triggers a compiler error.

See also

  • Using auto whenever possible to understand how automatic type deduction works in C++
  • Understanding the various forms of non-static member initialization to learn how to best perform initialization of class members