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

Chapter 5 Advanced Concepts of Modern C++

The noexcept specifier in a function’s signature declares that the function may not throw any exceptions. The same is valid for noexcept(true), which is just a synonym for noexcept. Instead, a function that is declared with noexcept(false) is potentially throwing, that is, it may throw exceptions. Here are some examples:

void nonThrowingFunction() noexcept;

void anotherNonThrowingFunction() noexcept(true);

void aPotentiallyThrowingFunction() noexcept(false); // The default if nothing has been specified.

There are two good reasons for using noexcept: First, exceptions that a function or method could throw (or not) are parts of the function’s interface. It is about semantics, and helps a developer who’s reading the code to know what might happen and what not might happen. noexcept tells developers that they can safely use this function in their own non-throwing functions. Hence, the presence of noexcept is somewhat akin to const.

Second, it can be used by the compiler for optimizations. noexcept potentially allows a compiler to compile the function without adding the runtime overhead that was formerly required by the removed throw(...); that is, the object code that was necessary to call std::unexpected() when an exception that was not listed was thrown.

For template implementers, there is also a noexcept operator, which performs a compile-­ time check that returns true if the expression is declared to not throw any exceptions:

constexpr auto isNotThrowing = noexcept(nonThrowingFunction());

Note  constexpr functions (see the section entitled “Computations During Compile Time”) can also throw when evaluated at runtime, so you may also need noexcept for some of those.

An Exception Is an Exception, Literally!

In Chapter 4, we discussed in the section “Do Not Pass or Return 0 (NULL, nullptr),” that you should not return nullptr as a return value from a function. As a code example, we had a small function that should perform a lookup for a customer by name, which of

202

Chapter 5 Advanced Concepts of Modern C++

course leads to no result if this customer cannot be found. Someone could now come up with the idea that we could throw an exception for a non-found customer, as shown in the following code example.

#include "Customer.h"

#include <string>

#include <exception>

class CustomerNotFoundException : public std::exception { private:

const char* what() const noexcept override { return "Customer not found!";

}

};

// ...

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

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

//...and if the customer could not be found: throw CustomerNotFoundException();

}

Now let’s take a look at the invocation site of this function:

Customer customer; try {

customer = findCustomerByName("Non-existing name");

}catch (const CustomerNotFoundException& ex) { // ...

}

// ...

At first sight, this seems to look like a feasible solution. If we have to avoid returning nullptr from the function, we can throw a CustomerNotFoundException instead. At the invocation site, we are now able to distinguish between the happy case and the bad case with the help of a try-catch construct.

In fact, it is a really bad solution! Not finding a customer just because its name does not exist is definitely no exceptional case. These are things that will happen normally.

203

Chapter 5 Advanced Concepts of Modern C++

Just think about a search function for users of a software application that deals with customers and allows a free text search.

What has been done in the previous example is an abuse of exceptions. Exceptions are not there to control the normal program flow. Exceptions should be reserved for what’s truly exceptional!

What does “truly exceptional” mean? Well, it means that there is nothing you can do about it, and you cannot really handle that exception. For instance, let’s assume that you are confronted with a std::bad_alloc exception, which means that there was a failure to allocate memory. How should the program continue now? What was the root cause for this problem? Does the underlying hardware system have a lack of memory? Well, then we have a really serious problem! Is there any meaningful way to recover from this serious exception and resume the program’s execution? Can we still take responsibility if the program simply continues running as if nothing happened?

These questions cannot be answered easily. Perhaps the real trigger for this problem was a dangling pointer, which has been used inexpertly millions of instructions before we’ve encountered the std::bad_alloc exception. All of this can seldom be reproduced at the time of the exception.

Tip  Throw exceptions only in very exceptional cases. Do not misuse exceptions to control the normal program flow.

You might wonder now, it is bad to use nullptr and NULL as a return value, and exceptions are also undesired, then what should I do instead? In the section entitled “Special Case Object (Null Object)” in Chapter 9 about design patterns, I present a feasible solution to handle these cases in a proper way.

If You Can’t Recover, Get Out Quickly

If you are confronted with an exception from which you cannot recover, it is often the best approach to log the exception (if possible), or to generate a crash dump file for later analyzing purposes, and to terminate the program immediately. A good example where a quick termination can be the best reaction is a failed memory allocation. If a system lacks memory, well, what should you do in the context of your program?

The principle behind this strict handling strategy for some critical exceptions and errors is called “Dead Programs Tell No Lies” and is described in the book Pragmatic Programmer [Hunt99].

204

Chapter 5 Advanced Concepts of Modern C++

Nothing is worse than continuing after a serious error as if nothing had happened, and to produce, for example, tens of thousands of erroneous bookings, or to send the lift for the hundredth time from the cellar to the top floor and back. Instead, get out before too much consequential damage occurs.

Define User-Specific Exception Types

Although you can throw whatever you want in C++, like an int or a const char*, I would not recommend it. Exceptions are caught by types; hence it is a very good idea to create your custom exception classes for certain, mostly domain-specific, exceptions. As I explained in Chapter 4, good naming is crucial for the readability and the maintainability of the code, and exception types should have good names. Further principles, which are valid for designing the “normal” program code, are of course also valid for exception types (we discuss these principles in detail in the Chapter 6 about object orientation).

To provide your own exception type, you can simply create your own class and derive them from std::exception (defined in the <stdexcept> header):

#include <stdexcept>

class MyCustomException : public std::exception { public:

const char* what() const noexcept override {

return "Provide some details about what was going wrong here!";

}

};

By overriding the virtual what() member function inherited from std::exception, we can provide some information to the caller about what went wrong. Furthermore, deriving our own exception class from std::exception will make it catchable by a generic catch clause (which, by the way, should only be regarded as the very last possibility to catch an exception), like this one:

#include <iostream>

// ...

try { doSomethingThatThrows();

205

Chapter 5 Advanced Concepts of Modern C++

}catch (const std::exception& ex) { std::cerr << ex.what() << std::endl;

}

Basically, exception classes should have a simple design, but if you want to provide more details about the cause of the exception, you can also write more sophisticated classes, like the one in Listing 5-29.

Listing 5-29.  A Custom Exception Class for Divisions by Zero

class DivisionByZeroException : public std::exception { public:

DivisionByZeroException() = delete;

explicit DivisionByZeroException(const int dividend) { buildErrorMessage(dividend);

}

const char* what() const noexcept override { return errorMessage.c_str();

}

private:

void buildErrorMessage(const int dividend) { errorMessage = "A division with dividend = "; errorMessage += std::to_string(dividend);

errorMessage += ", and divisor = 0, is not allowed (Division by Zero)!";

}

std::string errorMessage;

};

Note that due to its implementation, the private member function buildErrorMessage() can only guarantee strong exception safety, that is, it may throw due to the use of std::string::operator+=()! Hence, the initialization constructor cannot give the no-throw guarantee. That’s why exception classes generally should have a pretty simple design.

206