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

Chapter 4 Basics of Clean C++

template <typename Type, std::size_t size>

constexpr Type* begin(Type (&cArray)[size]) noexcept { return cArray;

}

template <typename Type, std::size_t size> constexpr Type* end(Type (&cArray)[size]) noexcept {

return cArray + size;

}

To insert all elements of the array into an output stream, for example, to print them on standard output, we can now write:

using namespace std;

int main() {

for (auto it = begin(fibonacci); it != end(fibonacci); ++it) { std::cout << *it << ", ";

}

std::cout << std::endl; return 0;

}

Providing overloaded begin() and end() functions for custom container types, or old C-style arrays, enables the application of all Standard Library algorithms to these types.

Furthermore, std::array can access elements including bound checks with the help of the std::array::at(size_type n) member function. If the given index is out of bounds, an exception of type std::out_of_bounds is thrown.

Use C++ Casts Instead of Old C-Style Casts

Before a false impression emerges, I would first like to state an important warning.

Warning  Type casts are basically bad and should be avoided whenever possible! They are a trustworthy indication that there must be, albeit a relatively tiny, design problem.

125

Chapter 4 Basics of Clean C++

However, if a type cast cannot be avoided in a certain situation, then under no circumstances should you use a C-style cast:

double d { 3.1415 }; int i = (int)d;

In this case, the double is demoted to an integer. This explicit conversion is accompanied with a loss of precision since the decimal places of the floating-­ point number are thrown away. The explicit conversion with the C-style cast says

something like this: “The programmer who wrote this line of code was aware about the consequences.”

Well, this is certainly better than an implicit type conversion. Nevertheless, instead using old C-style casts, you should use C++ casts for explicit type conversions, like this:

int i = static_cast<int>(d);

The simple explanation for this advice is, with the exception of the dynamic_cast<T>, the compiler checks C++ style casts during compile time! C-style casts are not checked this way and thus they can fail at runtime, which may cause ugly bugs or application crashes. For instance, an improvident used C-style cast can cause a corrupted stack, like in the following case.

int32_t i {

200 };

// Reserves and uses 4

byte

memory

int64_t* pointerToI = (int64_t*)&i;

//

Pointer points to 8

byte

 

*pointerToI

= 9223372036854775807;

//

Can cause run-time error

through

 

 

 

stack corruption

 

 

Obviously, in this case it is possible to write a 64-bit value into a memory area that is only 32 bits in size. The problem is that the compiler cannot draw our attention to this potentially dangerous piece of code. The compiler translates this code, even with very conservative settings (g++ -std=c++17 -pedantic -pedantic-errors -Wall -Wextra -Werror -Wconversion), without complaints. This can lead to very insidious errors during program execution.

Now let’s see what will happen if we use a C++ static_cast on the second line instead of the old and bad C-style cast:

int64_t* pointerToI = static_cast<int64_t*>(&i); // Pointer points to 8 byte

126

Chapter 4 Basics of Clean C++

The compiler can now spot the problematic conversion and report a corresponding error message:

error: invalid static_cast from type 'int32_t* {aka int*}' to type 'int64_t* {aka long int*}'

Another reason that you should use C++ casts instead of old C-style casts is that C-style casts are very hard to spot in a program. In addition, developers cannot easily discover them, nor can they search them conveniently using an ordinary editor or word processor. In contrast, it is very easy to search for terms such as static_cast<>, const_cast<>, or dynamic_cast<>.

At a glance, here is the advice regarding type conversions for a modern and well-­designed C++ program:

•\ Try to avoid type conversions (casts) under all circumstances.

Instead, try to eliminate the underlying design error that forces you to use the conversion.

•\ If an explicitly type conversion cannot be avoided, use C++ style casts (static_cast<> or const_cast<>) only, because the compiler checks these casts. Never use old and bad C-style casts.

•\ Notice that dynamic_cast<> should also never be used because it is considered bad design. The need of a dynamic_cast<> is a reliable indication that something is wrong within a specialization hierarchy. (This topic will be deepened in Chapter 6 about object orientation.)

•\ Do not use reinterpret_cast<> under any circumstances.

This kind of type conversion marks an unsafe, non-portable, and implementation-dependent cast. Its long and inconvenient name is a broad hint to make you think about what you’re currently doing. If you have to interpret an object bit by bit as another object, use

std::bit_cast<> (new since C++20) instead of reinterpret_cast<> or std::memcpy(). std::bit_cast<> (defined in the <bit> header) can be evaluated at compile-time (constexpr) and requires that the objects involved be trivially copied and be the same size.

127

Chapter 4 Basics of Clean C++

Avoid Macros

Maybe one of the severest legacies of the C language is macros. A macro is a code fragment that can be identified by a name. If the so-called preprocessor finds the name of a macro in the program’s source code while compiling, the name is replaced by its related code fragment.

One kind of macro is the object-like macro often used to give symbolic names to numeric constants, as shown in Listing 4-31.

Listing 4-31.  Two Examples of Object-Like Macros

#define BUFFER_SIZE 1024 #define PI 3.14159265358979

Other typical examples of macros are shown in Listing 4-32.

Listing 4-32.  Two Examples of Function-Like Macros

#define MIN(a,b) (((a)<(b))?(a): (b)) #define MAX(a,b) (((a)>(b))?(a): (b))

MIN and MAX compare two values and returns the smaller and larger one, respectively. Such macros are called function-like macros. Although these macros look almost like functions, they are not. The C preprocessor merely substitutes the name with the related code fragment (in fact, it is a textual find-and-replace operation).

Macros are potentially dangerous. They often do not behave as expected and can have unwanted side effects. For instance, let’s assume that you defined a macro like this one:

#define DANGEROUS 1024+1024

And somewhere in your code you write this:

int value = DANGEROUS * 2;

Probably someone expects that the variable value contains 4096, but actually it would be 3072. Remember the order of mathematical operations, which tells us that division and multiplication, from left to right, should happen first.

Another example of unexpected side effects due to using a macro is using MAX in the following way:

int maximum = MAX(12, value++);

128

Chapter 4 Basics of Clean C++

The preprocessor will generate the following:

int maximum = (((12)>(value++))?(12):(value++));

As can easily be seen now, the post-increment operation on value will be performed twice. This was certainly not the intention of the developer who wrote the piece of code.

Don’t use macros anymore! At least since C++11, they are almost obsolete. With some very rare exceptions, macros are simply no longer necessary and should no longer be used in a modern C++ program. Maybe the introduction of so-called reflection (i.e., the ability of a program to examine, introspect, and modify its own structure and behavior at runtime) as a possible part of a future C++ standard can help to get rid of macros entirely. But until the time comes, macros are still currently needed for some special purposes, for example, when using a unit test or logging framework.

STD::SOURCE_LOCATION [C++20]

Since C++20, it is also possible to do without the probably well-known C macros __FILE__ and __LINE__. As a reminder: The preprocessor expands the macro __FILE__ to the filename of the source code file and the macro __LINE__ to the current line number.

Both macros were typically used when log statements and error messages intended for programmers were generated.

With C++20 we now get a modern replacement in the form of a class: std::source_ location (defined in the <source_location> header). The class has a static factory method called std::source_location::current(), which is designed as a so-called immediate function (consteval). It can be used to create a new std::source_location object at compile-time that contains information corresponding to the location of the call site:

#include <source_location>;

// ...and somewhere in the code:

const auto& location = std::source_location::current();

The class provides four public member functions (file_name(), function_name(), column(), and line()), which can then be used to retrieve the information for output or logging purposes.

std::cout << "Filename: " << location.file_name()

<<", Function: " << location.function_name()

<<", Line/Column: (" << location.line() << "," << location.column() << ")\n";

129

Chapter 4 Basics of Clean C++

Instead of object-like macros, use constant expressions to define constants:

constexpr int HARMLESS = 1024 + 1024;

And instead of function-like macros, simply use true functions, for example, the function templates std::min or std::max, which are defined in the <algorithm> header (see the section about the <algorithm> header in Chapter 5):

#include <algorithm> // ...

int maximum = std::max(12, value++);

130

CHAPTER 5

Advanced Concepts

of Modern C++

In Chapters 3 and 4, we discussed the basic principles and practices that build a solid foundation for clean and modern C++ code. With these principles and rules in mind, a developer can raise the internal C++ code quality of a software project and, thus often its external quality, significantly. The code becomes more understandable, more

maintainable, more easily extensible, and less susceptible to bugs. This leads to a better life for any software crafter, because it is more fun to work with a sound code base. In Chapter 2, we learned that, above all, a well-maintained suite of well-crafted unit tests can further improve the quality of the software as well as the development efficiency.

But can we do better? Of course we can.

As I explained in this book’s introduction, the good old dinosaur C++ has experienced some considerable improvements during the last decade. The C++11 language standard (short for ISO/IEC 14882:2011) has fundamentally revolutionized the way developers think about C++ programming. After the rather evolutionary developments C++14 and C++17, and now with C++20, the standard contains many innovations and changes.

I have used a few of the C++ standards and features in the previous chapters and explained them in sidebars. Now it is time to dive deeper into some of them and explore how they can help you write exceptionally sound and modern C++ code. Of course, it is not possible to discuss all the language features of the newer C++ standards completely. That would go far beyond the scope of this book, leaving aside the fact that this is covered by numerous other books. Furthermore, it should not be the goal to use every fancy feature of the new C++ standards in every program. Always think about the KISS principle described in Chapter 3. Therefore, I have selected a few topics that I believe support the goal of writing clean C++ code very well.

131

© Stephan Roth 2021

S. Roth, Clean C++20, https://doi.org/10.1007/978-1-4842-5949-8_5