Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
roth_stephan_clean_c20_sustainable_software_development_patt.pdf
Скачиваний:
29
Добавлен:
27.03.2023
Размер:
7.26 Mб
Скачать

Chapter 5 Advanced Concepts of Modern C++

Be careful! If you define a type alias by writing something like the following, the std::shared_ptr<T> will manage something that is of type void**, because HANDLE is defined as a void-pointer:

using Win32SharedHandle = std::shared_ptr<HANDLE>; // Caution!

Therefore, smart pointers for the Win32 HANDLE must be defined as follows:

using Win32SharedHandle = std::shared_ptr<void>; using Win32WeakHandle = std::weak_ptr<void>;

Note  You cannot define a std::unique_ptr<void> in C++! This is because std::shared_ptr<T> implements type-erasure, while std::unique_ptr<T> does not. If a class supports type-erasure, it means that it can store objects of an arbitrary type and destruct them correctly.

If you want to use the shared handle, you have to pay attention that you pass an instance of the custom deleter Win32HandleCloser as a parameter during construction:

const DWORD processId = 4711;

Win32SharedHandle processHandle { OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId),

Win32HandleCloser() };

We Like to Move It

If someone asked me which C++11 feature has the most profound impact on how modern C++ programs are written now and in the future, I would clearly nominate move semantics. I discussed C++ move semantics briefly in Chapter 4, in the section about strategies to avoid regular pointers. But I think that they are so important that I want to deepen this language feature here.

What Are Move Semantics?

In many cases where the old C++ language forced us to use a copy constructor, we actually did not really want to create a deep copy of an object. Instead, we simply wanted

146

Chapter 5 Advanced Concepts of Modern C++

to “move the object’s payload.” An object’s payload is nothing else than the embedded data that the object carries around with it, so nothing else than other objects or member variables of primitive types like int.

These cases where we had to copy an object instead of moving it were, for example, the following:

•\

Returning a local object instance as a return value from a function or

 

method. To prevent the copy construction in these cases prior C++11,

 

pointers were frequently used.

•\

Inserting an object into a std::vector or other containers.

•\

The implementation of the std::swap<T> template function.

In many of the before-mentioned situations, it is unnecessary to keep the source object intact, that is, to create a deep, and in terms of runtime efficiency often costly, copy so that the source objects remains usable.

C++11 introduced a language feature that makes moving an object’s embedded data a first-class operation. In addition to the copy constructor and copy assignment operator, the class’s developer can now implement move constructors and move assignment operators (we will see later why we actually should not do that!). The move operations are usually very efficient. In contrast to a real copy operation, the source object’s data

is just handed over to the target object, and the argument (the source object) of the operation is put into a kind of “empty” or initial state.

The example in Listing 5-9 shows an arbitrary class that explicitly implements both types of semantics: copy constructor (line 6) and assignment operator (line 8), as well as move constructor (line 7) and assignment operator (line 9).

Listing 5-9.  An Example Class That Explicitly Declares Special Member Functions for Copy and Move

01

#include <string>

 

02

 

 

03

class Clazz {

 

04

public:

 

05

Clazz() noexcept;

// Default constructor

06

Clazz(const Clazz& other);

// Copy constructor

07

Clazz(Clazz&& other) noexcept;

// Move constructor

08

Clazz& operator=(const Clazz& other);

// Copy assignment operator

147

Chapter 5 Advanced Concepts of Modern C++

09

Clazz& operator=(Clazz&& other) noexcept; //

Move assignment operator

10

virtual ~Clazz() noexcept;

//

Destructor

11

 

 

 

12private:

13// ...

14};

Note  The noexcept specifier specifies whether a function can throw exceptions or not and is explained in more detail in the section entitled “The No-Throw Guarantee” later in this chapter.

As you will see later in the section “The Rule of Zero,” it should be a major goal of any C++ developer to not declare and define such constructors and assignment operators explicitly.

The move semantics are closely related to something that is called rvalue references (see the next section). The constructor or assignment operator of a class is called a “move constructor” or a “move assignment operator,” respectively, when it takes an rvalue reference as a parameter. An rvalue reference is marked through the double ampersand operator (&&). For better distinction, the ordinary reference with its single ampersand (&) is now also called an lvalue reference.

The Matter with Those lvalues and rvalues

The lvalues and rvalues are historical terms (inherited from language C), because lvalues could usually appear on the left side of an assignment expression, whereas rvalues could usually appear on the right side of an assignment expression. In my opinion, a much better explanation for lvalue is that it is a locator value. This makes it clear that an lvalue represents an object that occupies a location in memory (i.e., it has an accessible and identifiable memory address).

In contrast, rvalues are all those objects in an expression that are not lvalues. They are temporary objects, or subobjects thereof. Hence, it is not possible to assign anything to an rvalue.

148

Chapter 5 Advanced Concepts of Modern C++

Although these definitions come from the old C world, and C++11 still has introduced more categories (xvalue, glvalue, and prvalue) to enable move semantics, they are pretty good for everyday use.

The simplest form of an lvalue expression is a variable declaration:

Type var1;

The expression var1 is an lvalue of type Type. The following declarations represent lvalues too:

Type* pointer;

Type& reference;

Type& function();

An lvalue can be the left operand of an assignment operation, like the integer-­ variable theAnswerToAllQuestions in this example:

int theAnswerToAllQuestions = 42;

The assignment of a memory address to a pointer also makes clear that the pointer is an lvalue:

Type* pointerToVar1 = &var1;

The literal “42” instead is an rvalue. It doesn’t represent an identifiable location in memory, so it is not possible to assign anything to it (of course, rvalues also occupy

memory in the data section on the stack, but this memory is allocated temporarily and released immediately after completion of the assignment operation):

int number = 23; // Works, because 'number' is an lvalue

42 = number; // Compiler error: lvalue required as left operand of assignment

You don’t believe that function() on the third line from the above generic examples is an lvalue? It is! You can write the following (without doubt, some kind of weird) piece of code and the compiler will compile it without complaints:

int theAnswerToAllQuestions = 42;

int& function() {

return theAnswerToAllQuestions;

}

149