Book Image

C++ High Performance - Second Edition

By : Björn Andrist, Viktor Sehr
5 (2)
Book Image

C++ High Performance - Second Edition

5 (2)
By: Björn Andrist, Viktor Sehr

Overview of this book

C++ High Performance, Second Edition guides you through optimizing the performance of your C++ apps. This allows them to run faster and consume fewer resources on the device they're running on without compromising the readability of your codebase. The book begins by introducing the C++ language and some of its modern concepts in brief. Once you are familiar with the fundamentals, you will be ready to measure, identify, and eradicate bottlenecks in your C++ codebase. By following this process, you will gradually improve your style of writing code. The book then explores data structure optimization, memory management, and how it can be used efficiently concerning CPU caches. After laying the foundation, the book trains you to leverage algorithms, ranges, and containers from the standard library to achieve faster execution, write readable code, and use customized iterators. It provides hands-on examples of C++ metaprogramming, coroutines, reflection to reduce boilerplate code, proxy objects to perform optimizations under the hood, concurrent programming, and lock-free data structures. The book concludes with an overview of parallel algorithms. By the end of this book, you will have the ability to use every tool as needed to boost the efficiency of your C++ projects.
Table of Contents (17 chapters)
15
Other Books You May Enjoy
16
Index

Automatic type deduction with the auto keyword

Since the introduction of the auto keyword in C++11, there has been a lot of confusion in the C++ community about how to use the different flavors of auto, such as const auto&, auto&, auto&&, and decltype(auto).

Using auto in function signatures

Although discouraged by some C++ programmers, in my experience the use of auto in function signatures can increase readability when browsing and viewing header files.

Here is how the auto syntax looks compared to the traditional syntax with explicit types:

Traditional syntax with explicit type:

New syntax with auto:

struct Foo {
  int val() const {    return m_;   }  const int& cref() const {    return m_;   }  int& mref() {    return m_;   }  int m_{};};
struct Foo {
  auto val() const {    return m_;   }  auto& cref() const {    return m_;   }  auto& mref() {    return m_;   }  int m_{};};

The auto syntax can be used both with and without a trailing return type. The trailing return is necessary in some contexts. For example, if we are writing a virtual function, or the function declaration is put in a header file and the function definition is in a .cpp file.

Note that the auto syntax can also be used with free functions:

Return type

Syntactic variants (a, b, and c correspond to the same result):

Value

auto val() const                // a) auto, deduced type
auto val() const -> int         // b) auto, trailing type
int val() const                 // c) explicit type

Const reference

auto& cref() const              // a) auto, deduced type
auto cref() const -> const int& // b) auto, trailing type
const int& cref() const         // c) explicit type

Mutable reference

auto& mref()                    // a) auto, deduced type
auto mref() -> int&             // b) auto, trailing type
int& mref()                     // c) explicit type

Forwarding the return type using decltype(auto)

There is a somewhat rare version of automatic type deduction called decltype(auto). Its most common use is for forwarding the exact type from a function. Imagine that we are writing wrapper functions for val() and mref() declared in the previous table, like this:

int val_wrapper() { return val(); }    // Returns int
int& mref_wrapper() { return mref(); } // Returns int&

Now, if we wanted to use return type deduction for the wrapper functions, the auto keyword would deduce the return type to an int in both cases:

auto val_wrapper() { return val(); }   // Returns int
auto mref_wrapper() { return mref(); } // Also returns int

If we wanted our mref_wrapper() to return an int&, we would need to write auto&. In this example, this would be fine, since we know the return type of mref(). However, that's not always the case. So if we want the compiler to instead choose the exact same type without explicitly saying int& or auto& for mref_wrapper(), we can use decltype(auto):

decltype(auto) val_wrapper() { return val(); }   // Returns int
decltype(auto) mref_wrapper() { return mref(); } // Returns int&

In this way, we can avoid explicitly choosing between writing auto or auto& when we don't know what the function val() or mref() return. This is a scenario that usually happens in generic code where the type of the function that is being wrapped is a template parameter.

Using auto for variables

The introduction of the auto keyword in C++11 has initiated quite a debate among C++ programmers. Many people think it reduces readability, or even that it makes C++ similar to a dynamically typed language. I tend to not participate in those debates, but my personal opinion is that you should (almost) always use auto as, in my experience, it makes the code safer and less littered with clutter.

Overusing auto can make the code harder to understand. When reading code, we usually want to know which operations are supported by some object. A good IDE can provide us with this information, but it's not explicitly there in the source code. C++20 concepts address this issue by focusing on the behavior of an object. See Chapter 8, Compile-Time Programming, for more information about C++ concepts.

I prefer to use auto for local variables using the left-to-right initialization style. This means keeping the variable on the left, followed by an equals sign, and then the type on the right side, like this:

auto i = 0;
auto x = Foo{};
auto y = create_object();
auto z = std::mutex{};     // OK since C++17

With guaranteed copy elision introduced in C++17, the statement auto x = Foo{} is identical to Foo x{}; that is, the language guarantees that there is no temporary object that needs to be moved or copied in this case. This means that we can now use the left-to-right initialization style without worrying about performance and we can also use it for non-movable/non-copyable types, such as std::atomic or std::mutex.

One big advantage of using auto for variables is that you will never leave a variable uninitialized since auto x; doesn't compile. Uninitialized variables are a particularly common source of undefined behavior that you can completely eliminate by following the style suggested here.

Using auto will help you with using the correct type for your variables. What you still need to do, though, is to express how you intend to use a variable by specifying whether you need a reference or a copy, and whether you want to modify the variable or just read from it.

A const reference

A const reference, denoted by const auto&, has the ability to bind to anything. The original object can never be mutated through such a reference. I believe that the const reference should be the default choice for objects that are potentially expensive to copy.

If the const reference is bound to a temporary object, the lifetime of the temporary will be extended to the lifetime of the reference. This is demonstrated in the following example:

void some_func(const std::string& a, const std::string& b) {
  const auto& str = a + b;  // a + b returns a temporary
  // ...
} // str goes out of scope, temporary will be destroyed

It's also possible to end up with a const reference by using auto&. This can be seen in the following example:

 auto foo = Foo{};
 auto& cref = foo.cref(); // cref is a const reference
 auto& mref = foo.mref(); // mref is a mutable reference

Even though this is perfectly valid, it is preferable to always explicitly express that we are dealing with const references by using const auto&, and, more importantly, we should use auto& to only denote mutable references.

A mutable reference

In contrast to a const reference, a mutable reference cannot bind to a temporary. As mentioned, we use auto& to denote mutable references. Use a mutable reference only when you intend to change the object it references.

A forwarding reference

auto&& is called a forwarding reference (also referred to as a universal reference). It can bind to anything, which makes it useful for certain cases. Forwarding references will, just like const references, extend the lifetime of a temporary. But in contrast to the const reference, auto&& allows us to mutate objects it references, temporaries included.

Use auto&& for variables that you only forward to some other code. In those forwarding cases, you rarely care about whether the variable is a const or a mutable; you just want to pass it to some code that is actually going to use the variable.

It's important to note that auto&& and T&& are only forwarding references if used in a function template where T is a template parameter of that function template. Using the && syntax with an explicit type, for example std::string&&, denotes an rvalue reference and does not have the properties of a forwarding reference (rvalues and move semantics will be discussed later in this chapter).

Practices for ease of use

Although this is my personal opinion, I recommend using const auto for fundamental types (int, float, and so on) and small non-fundamental types like std::pair and std::complex. For bigger types that are potentially expensive to copy, use const auto&. This should cover the majority of the variable declarations in a C++ code base.

auto& and auto should only be used when you require the behavior of a mutable reference or an explicit copy; this communicates to the reader of the code that those variables are important as they either copy an object or mutate a referenced object. Finally, use auto&& for forwarding code only.

Following these rules makes your code base easier to read, debug, and reason about.

It might seem odd that while I recommend using const auto and const auto& for most variable declarations, I tend to use a simple auto in some places in this book. The reason for using plain auto is the limited space that the format of a book provides.

Before moving on, we will spend a little time talking about const and how to propagate const when using pointers.

Const propagation for pointers

By using the keyword const, we can inform the compiler about which objects are immutable. The compiler can then check that we don't try to mutate objects that aren't intended to be changed. In other words, the compiler checks our code for const-correctness. A common mistake when writing const-correct code in C++ is that a const-initialized object can still manipulate the values that member pointers point at. The following example illustrates the problem:

class Foo {
public:
  Foo(int* ptr) : ptr_{ptr} {} 
  auto set_ptr_val(int v) const { 
    *ptr_ = v; // Compiles despite function being declared const!
  }
private:
  int* ptr_{};
};
int main() {
  auto i = 0;
  const auto foo = Foo{&i};
  foo.set_ptr_val(42);
}

Although the function set_ptr_val() is mutating the int value, it's valid to declare it const since the pointer ptr_ itself is not mutated, only the int object that the pointer is pointing at.

In order to prevent this in a readable way, a wrapper called std::experimental::propagate_const has been added to the standard library extensions (included in, at the time of writing, the latest versions of Clang and GCC). Using propagate_const, the function set_ptr_val() will not compile. Note that propagate_const only applies to pointers, and pointer-like classes such as std::shared_ptr and std::unique_ptr, but not std::function.

The following example demonstrates how propagate_const can be used to generate compilation errors when trying to mutate an object inside a const function:

#include <experimental/propagate_const>
class Foo { 
public: 
  Foo(int* ptr) : ptr_{ptr} {}
  auto set_ptr(int* p) const { 
    ptr_ = p;  // Will not compile, as expected
  }
  auto set_val(int v) const { 
    val_ = v;  // Will not compile, as expected
  }
  auto set_ptr_val(int v) const { 
    *ptr_ = v; // Will not compile, const is propagated
  }
private:
  std::experimental::propagate_const<int*> ptr_ = nullptr; 
  int val_{}; 
};

The importance of proper use of const in large code bases cannot be overstated, and the introduction of propagate_const makes const-correctness even more effective.

Next, we will have a look at move semantics and some important rules for handling resources inside a class.