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

Chapter 4 Basics of Clean C++

Avoid Output Parameters

An output parameter, sometimes also called a result parameter, is a function parameter that is used for the function’s return value.

One of the frequently mentioned benefits of using output parameters is that functions that use them can pass back more than one value at a time. Here is a typical example:

bool ScriptInterpreter::executeCommand(const std::string& name, const std::vector<std::string>& arguments, Result& result);

This member function of the ScriptInterpreter class returns not only a bool. The third parameter is a non-const reference to an object of type Result, which represents the real result of the function. The Boolean return value determines whether the execution of the command was successful by the interpreter. A typical call of this member function might look like this:

ScriptInterpreter interpreter; // Many other preparations...

Result result;

if (interpreter.executeCommand(commandName, argumentList, result)) {

//Continue normally...

}else {

//Handle failed execution of command...

Tip  Avoid output parameters at all costs.

Output parameters are unintuitive and can lead to confusion. The caller can sometimes not determine whether a passed object is treated as an output parameter and will possibly be mutated by the function.

Furthermore, output parameters complicate the easy composition of expressions. If functions have only one return value, they can be interconnected quite easily to chained function calls. In contrast, if functions have multiple output parameters, developers

are forced to prepare and handle all the variables that will hold the resultant values. Therefore, the code that calls these functions can turn into a mess quickly.

103

Chapter 4 Basics of Clean C++

Especially if immutability should be fostered and side effects must be reduced, output parameters are an absolutely terrible idea. Unsurprisingly, it is still impossible to pass an immutable object (see Chapter 9) as an output parameter.

If a method should return something to its callers, let the method return it as the method’s return value. If the method must return multiple values, redesign it to return a single instance of an object that holds the values.

Alternatively, the class template std::pair can be used. The first member variable is assigned the Boolean value indicating success or fail, and the second member variable is assigned the real return value. However, both std::pair and its “big brother” std::tuple (available since C++11) are, from my point of view, always a design smell. A std::pair<bool, Result> is not really a speaking name. If you decide to use something like that, and I would not recommend it anyway, you should at least introduce a meaningful alias name with the help of the using declaration.

Another possibility is to use a std::optional, a class template that is defined in the <optional> header and available since C++17. As its name suggest, objects of this class template can manage an optional contained value, i.e., a value that may or may not be present.

In addition to the aforementioned solutions, there is one more. You can use the so-called special case object pattern to return an object representing an invalid result. Since this is a object-oriented design pattern, I introduce it in Chapter 9.

Here is my final advice about how to deal with return parameters: As mentioned, avoid output parameters. If you want to return multiple values from a function or method, introduce a small class with well-named member variables to bundle all the data that you want to return to the call site. You may find after a short while that this class should have existed anyway and you can put some logic in it.

Don’t Pass or Return 0 (NULL, nullptr)

THE BILLION DOLLAR MISTAKE

Sir Charles Antony Richard Hoare, commonly known as Tony Hoare or C. A. R. Hoare, is a famous British computer scientist. He is primarily known for the Quick Sort algorithm. In 1965, Tony Hoare worked with the Swiss computer scientist Niklaus E. Wirth on the further development of the programming language ALGOL. He introduced null references in the programming language ALGOL W, which was the predecessor of PASCAL.

104

Chapter 4 Basics of Clean C++

More than 40 years later, Tony Hoare regrets this decision. In a talk at the QCon 2009 Conference in London, he said that the introduction of null references had probably been a billion dollar mistake. He argued that null references have caused so many problems in the past decades that the cost could be approximated at $USD 1 billion.

In C++, pointers can point to NULL or 0. Concretely, this means that the pointer points to the memory address 0. NULL is just a macro definition:

#define NULL

0

Since C++11, the language provides the new keyword called nullptr, which is of type std::nullptr_t.

Sometimes I see functions like this one:

Customer* findCustomerByName(const std::string& name) const {

//Code that searches the customer by name...

//...and if the customer could not be found: return nullptr; // ...or NULL;

}

Receiving NULL or nullptr as a return value from a function can be confusing. (Starting from here, I will only use nullptr in the following text, because the C-style macro NULL has no place in modern C++ anymore.) What should the caller do with it? What does it mean? In the previous example, it might be that a customer with the given name does not exist. But it can also mean that there has been a critical error. A nullptr can mean failure, can mean success, and can mean almost anything.

Note  If it is inevitable to return a regular pointer as the result from a function or method, do not return nullptr!

In other words, if you’re forced to return a regular pointer as the result from a function (we will see later that there may be better alternatives), ensure that the pointer you’re returning always points to a valid address. Here are my reasons why I think this is important.

The main rationale why you should not return nullptr from a function is that you shift the responsibility to decide what to do to your callers. They have to check it. They

105

Chapter 4 Basics of Clean C++

have to deal with it. If functions can potentially return nullptr, this leads to many null checks, like this:

Customer* customer = findCustomerByName("Stephan");

if (customer != nullptr) {

OrderedProducts* orderedProducts = customer->getAllOrderedProducts(); if (orderedProducts != nullptr) {

//Do something with orderedProducts...

}else {

//And what should we do here?

}

}else {

//And what should we do here?

Many null checks reduce the readability of the code and increase its complexity. And there is another visible problem that leads us directly to the next point.

If a function can return a valid pointer or nullptr, it introduces an alternative flow path that needs to be continued by the caller. And it should lead to a reasonable and sensible reaction. This is sometimes quite problematic. What would be the correct, intuitive response in our program when our pointer to Customer is not pointing to a valid instance, but nullptr? Should the program abort the running operation with a message? Are there any requirements that a certain type of program continuation is mandatory in such cases? These questions sometimes cannot be answered well. Experience has shown that it is often relatively easy for stakeholders to describe all the so-called happy day cases of their software, which are the positive cases during normal operation. It is much more difficult to describe the desired behavior of the software during the exceptions, errors, and special cases.

The worst consequence may be this: If any null check is forgotten, this can lead to critical runtime errors. Dereferencing a null pointer will lead to a segmentation fault and your application will crash.

In C++ there is still another problem to consider: object ownership.

For the caller of the function, it is unclear what to do with the resource pointed to by the pointer after its usage. Who is its owner? Is it required to delete the object? If yes, how is the resource to be disposed? Must the object be deleted with delete, because it was allocated with the new operator somewhere inside the function? Or is the ownership of

106