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

Chapter 4 Basics of Clean C++

the resource object managed differently, so that a delete is forbidden and will result in undefined behavior (see the section “Don’t Allow Undefined Behavior” in Chapter 5)? Is it perhaps even an operating system resource that has to be handled in a very special manner?

According to the information hiding principle (see Chapter 3), this should have no relevance for the callers, but in fact we’ve imposed the responsibility for the resource to them. And if the callers do not handle the pointer correctly, it can lead to serious bugs, for example, memory leaks, double deletion, undefined behavior, and sometimes security vulnerabilities.

Strategies for Avoiding Regular Pointers

Choose simple object construction on the stack instead of on the heap

The simplest way to create a new object is simply by creating it on the stack, like so:

#include "Customer.h" // ...

Customer customer;

In this example, an instance of the Customer class (defined in the Customer.h header) is created on the stack. The line of code that creates the instance can usually be found somewhere inside a function’s or method’s body. That means that the instance is destroyed automatically if the function or method runs out of scope, which happens when we return from the function or method.

So far, so good. But what shall we do if an object that was created in a function or method must be returned to the caller?

In old-style C++, this challenge was often coped with in such a way that the object was created on the heap (using the new operator) and then returned from the function as a pointer to this allocated resource.

Customer* createDefaultCustomer() { Customer* customer = new Customer();

// Do something more with customer, e.g. configuring it, and at the end...

return customer;

}

107

Chapter 4 Basics of Clean C++

The comprehensible reason for this approach is that, if we are dealing with a large object, an expensive copy construction can be avoided this way. But we have already discussed the drawbacks of this solution in the previous section. For instance, what will the caller do if the returned pointer is nullptr? Furthermore, the caller of the function is forced to be in charge of the resource management (e.g., deleting the returned pointer in the correct manner).

COPY ELISION

Almost all, especially commercial-grade C++ compilers today, support so-called copy elision techniques. These are optimizations to prevent extra copies of objects in certain situations (depending on optimization settings; as of C++17, copy elision is guaranteed when an object is returned directly).

On the one hand, this is great, because we can get more performant software with less to no effort this way. And it makes returning by value or passing by value large and costly objects, apart from some exceptions, much simpler in practice. These exceptions are limitations of copy elision where this optimization won’t be able to kick in, such as having multiple exit points (return statements) in a function returning different named objects.

On the other hand, we have to keep in mind that copy elision—depending on the compiler and its settings—can influence the program’s behavior. If the copying an object is optimized away, any copy constructor that may be present is also not executed. Furthermore, if fewer objects are created, you can’t rely on a specific number of destructors being called. You shouldn’t put critical code inside copyor move-constructors or destructors, as you can’t rely on them being called. (You will learn that you should avoid implementing these so-called special member functions by hand anyway, in Chapter 5, in the section “The Rule of Zero”!)

Common forms of copy elision are return value optimization (RVO) and named return value optimization (NRVO).

108

Chapter 4 Basics of Clean C++

Named Return Value Optimization

NRVO eliminates the copy constructor and destructor of a named stack-based object that is returned. For instance, a function could return an instance of a class by value like in this simple example:

class SomeClass { public:

SomeClass();

SomeClass(const SomeClass&); SomeClass(SomeClass&&); ~SomeClass();

};

SomeClass getInstanceOfSomeClass() { SomeClass object;

return object;

}

RVO happens if a function returns a nameless temporary object, as with this modified form of the getInstanceOfSomeClass() function:

SomeClass getInstanceOfSomeClass() { return SomeClass ();

}

Important: Even when copy elision takes place and the call of a copy-/move-constructor is optimized away, they must be present and accessible, either hand-crafted or compiler-­ generated; otherwise, the program is considered ill-formed!

Good news: Since C++11, we can simply return large objects as values without being worried about a costly copy construction.

Customer createDefaultCustomer() { Customer customer;

// Do something with customer, and at the end...

return customer;

}

109

Chapter 4 Basics of Clean C++

The reason that we no longer have to worry about resource management in this case are the so-called move semantics, which are supported since C++11. Simply speaking, the concept of move semantics allows resources to be “moved” from one object to another instead of copying them. The term “move” means, in this context, that the internal data of an object is removed from the old source object and placed into a new object. It is a transfer of ownership of the data from one object to another object, and this can be performed extremely fast. (C++11 move semantics are discussed in detail in Chapter 5.)

With C++11, all Standard Library container classes have been extended to support move semantics. This not only has made them very efficient, but also much easier to handle. For instance, you can return a large vector containing strings from a function in a very efficient manner, as shown in the example in Listing 4-28.

Listing 4-28.  Since C++11, a Locally Instantiated and Large Object Can Be Easily Returned by Value

#include <vector> #include <string>

using StringVector = std::vector<std::string>;

const StringVector::size_type AMOUNT_OF_STRINGS = 10'000;

StringVector createLargeVectorOfStrings() { StringVector theVector(AMOUNT_OF_STRINGS, "Test");

return theVector; // Guaranteed no copy construction here!

}

The exploitation of move semantics is one very good way to get rid of lots of regular

pointers. But we can do much more...

In a function’s argument list, use (const) references instead of pointers

Instead of writing...

void function(Type* argument);

...you should use C++ references, like this:

void function(Type& argument);

110