In this chapter, we will take an in-depth look at some modern C++ concepts such as move-semantics, forwarding references, std::optional, std::any, and lambda functions. Some of these concepts still confuse even experienced C++ programmers and therefore we will look into both their use cases and how they work under the hood.
You're reading from C++ High Performance
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&, and auto&&.
Using auto in function signatures
Although discouraged by some C++ programmers, in our experience the use of auto in function signatures vastly increases readability when browsing and viewing header files.
Here is how the new auto syntax looks compared to the old syntax with explicit types:
Old syntax with explicit type: | New syntax with auto: |
struct Foo { |
The lambda function
The lambda function, introduced in C++11 and further enhanced with polymorphic capabilities in C++14, is one of the most useful features in modern C++. Its versatility comes not only from easily passing functions to algorithms but it can also be used in a lot of circumstances where you need to pass the code around, especially as you can store a lambda function in std::function.
Although the lambda function made these programming techniques vastly simpler to work with, everything here is possible to perform without them by making classes with operator() overloaded.
We will explore the lambda function's similarities to these kind of classes later, but first let's introduce the lambda function in a simple use case.
Basic syntax of a C++ lambda function
...Const propagation for pointers
A common mistake when writing const-correct code in C++ is that a const initialized object can still manipulate the values that member pointers points 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_{};
};
auto main() -> int {
const auto foo = Foo{};
foo.set_ptr_val(42);
}
Although the function set_ptr_val() is mutating the int value, it's valid to declared 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 std library extensions (included in, as of the time of writing this, the latest versions of...
Move semantics explained
Move semantics is a concept introduced in C++11 which, in our experience, is quite hard to grasp even by experienced programmers. Therefore, we will try to give you an in-depth explanation of how it works, when the compiler utilizes it, and, most importantly, why it is needed.
Essentially, the reason C++ even has the concept of move semantics, whereas most other languages don't, is a result of being a value-based language as discussed in Chapter 1, A Brief Introduction to C++. If C++ did not have move semantics built in, the advantages of value-based semantics would get lost in many cases and programmers would have to perform one of the following trade-offs:
- Performing redundant deep-cloning operations with high performance costs
- Using pointers for objects like Java do, losing the robustness of value semantics
- Performing error-prone swapping operations...
Representing optional values with std::optional
Although quite a minor feature in C++17, std::optional is a neat addition to the STL library which simplifies a common case which couldn't be expressed in a clean straightforward syntax prior to std::optional. In a nutshell, it is a small wrapper for any type where the wrapped type can be both initialized and uninitialized.
To put it in C++ lingo, std::optional is a stack-allocated container with a max size of one.
Optional return values
Before the introduction of std::optional, there was no clear way to define functions which may not return a defined value, such as...
Representing dynamic values with std::any
Just like std::optional, std::any can store an optional single value, but with the difference that it can store any type at runtime, just like a dynamically typed}language. As the std::any can withhold any type, you need to explicitly specify the type using the global function std::any_cast when reading the held object.
If the std::any is empty or withholds another type than the specified type, an exception is thrown.
Here is an example of how it works:
// Initialize an empty std::any
auto a = std::any{};
// Put a string in it
a = std::string{"something"};
// Return a reference to the withheld string
auto& str_ref = std::any_cast<std::string&>(a);
// Copy the withheld string
auto str_copy = std::any_cast<std::string>(a);
// Put a float in the 'any' and read it back
a = 135.246f;
auto flt = std::any_cast...
Summary
In this chapter you have learned how to use modern C++ features such as forwarding references, move-semantics, lambda functions, std::any, and std::optional. In the next chapter we will look into strategies for how to measure performance in C++.