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

Chapter 5 Advanced Concepts of Modern C++

std::cout << "The area of an American Football playing field is " << area << "m^2 and the length of its diagonal is " << diagonal << "m." << std::endl;

return 0;

}

constexpr classes can be used at compile time and at runtime. In contrast to ordinary classes, however, you cannot define virtual member functions (there is no polymorphism at compile time), and a constexpr class must not have an explicitly defined destructor.

Note  The code example in Listing 5-19 could fail to compile on some C++ compilers. By today’s standards, the C++ standard does not specify common mathematical functions from the numerics library (the <cmath> header) as constexpr, like std::sqrt() and std::pow(). Compiler implementations are free to do it anyway, but it’s not required.

However, how should these computations at compile time have been judged from a clean code perspective? Is it basically a good idea to add constexpr to anything that can possibly have it?

Well, my opinion is that constexpr does not reduce the readability of the code. The specifier is always in front of variables and constants definitions or in front of function or method declarations. Hence, it does not disturb so much. On the other hand, if I definitely know that something will never be evaluated at compile time, I should also renounce the specifier.

Don’t Allow Undefined Behavior

In C++ (and in some other programming languages too), the language specification does not define the behavior in any possible situation. In some places the specification says that the behavior of a certain operation is undefined under certain circumstances. In such a situation, you cannot predict what will happen, because the behavior of the program depends on compiler implementation, the underlying operating system, or special optimization switches. That’s really bad! The program could either crash or silently generate incorrect results.

169

Chapter 5 Advanced Concepts of Modern C++

Here is an example of undefined behavior, an incorrect use of a smart pointer:

const std::size_t NUMBER_OF_STRINGS { 100 }; std::shared_ptr<std::string> arrayOfStrings(new std::string[NUMBER_OF_ STRINGS]);

Let’s assume that this std::shared_ptr<T> object is the last one pointing to the string array resource and it runs out of scope somewhere. What will happen?

The destructor of std::shared_ptr<T> decrements the number of shared owners and the counter reaches 0. As a consequence, the resource managed by the smart pointer (the array of std::string) is destroyed by calling its destructor. But it will do it wrong, because when you allocate the managed resource using new[], you need to call the array form delete[], and not delete, to free the resource, and the default deleter of std::shared_ptr<T> uses delete.

Deleting an array with delete instead of delete[] results in undefined behavior. It is not specified what happens. Maybe it results in a memory leak, but that’s just a guess.

Caution  Avoid undefined behavior! It is a bad mistake and ends up with programs that silently misbehave.

There are several solutions to let the smart pointer delete the string array correctly. For example, you can provide a custom deleter as a function-like object (also known as a functor; see Chapter 7):

template <typename T> struct CustomArrayDeleter {

void operator() (T const* pointer) { delete [] pointer;

}

};

Now you can use your own deleter as follows:

const std::size_t NUMBER_OF_STRINGS { 100 }; std::shared_ptr<std::string> arrayOfStrings(new std::string[NUMBER_OF_ STRINGS], CustomArrayDeleter<std::string>());

170

Chapter 5 Advanced Concepts of Modern C++

In C++11, there is a default deleter for array types defined in the <memory> header:

const std::size_t NUMBER_OF_STRINGS { 100 }; std::shared_ptr<std::string> arrayOfStrings(new std::string[NUMBER_OF_ STRINGS], std::default_delete<std::string[]>());

Depending on the requirements to satisfy, you should consider whether using a std::vector or std::array is not the best solution to implement an “array of things.” And since C++20, you can avoid the explicit new for heap allocation and do it clean and simple like this:

auto arrayOfStrings{ std::make_shared<std::string[]>(NUMBER_OF_STRINGS) };

Type-Rich Programming

“Don’t trust names. Trust types.

Types don’t lie.

Types are your friends!”

—Mario Fusco (@mariofusco), April 13, 2016, on Twitter

On September 23, 1999, NASA lost its Mars Climate Orbiter I, a robotic space probe, after a 10-month journey to the fourth planet of our solar system (Figure 5-4). As the spacecraft went into orbital insertion, the transfer of important data failed between the propulsion team at Lockheed Martin Astronautics in Colorado and the NASA mission navigation team in Pasadena (California). This error pushed the spacecraft too close to the atmosphere of Mars, where it burned immediately.

171

Chapter 5 Advanced Concepts of Modern C++

Figure 5-4.  Artist’s rendering of the Mars Climate Orbiter (Author: NASA/JPL/ Corby Waste2)

The cause for the failed data transfer was that the NASA mission navigation team used the International System of Units (SI), while Lockheed Martin’s navigation software used English units (the Imperial Measurement System). The software used by the mission navigation team sent values in pound-force-seconds (lbf·s), but the Orbiter’s navigation software expected values in newton-seconds (N·s). NASA’s total financial loss was 328 million in U.S. dollars. The lifetime work of around 200 good spacecraft engineers was destroyed in a few seconds.

This failure is not a typical example of a simple software bug. Both systems by themselves may have worked correctly. But it reveals an interesting aspect in software development. It seems that communication and coordination problems between the engineering teams to be the elementary reason for this failure. It is obvious: there were no joint system tests with both subsystems, and the interfaces between both subsystems had not been properly designed.

2https://solarsystem.nasa.gov/resources/2246/mars-climate-orbiter-artists-concept/; https://www.nasa.gov/multimedia/guidelines/index.html

172

Chapter 5 Advanced Concepts of Modern C++

“People sometimes make errors. The problem here was not the error, it was the failure of NASA’s systems engineering, and the checks and balances in our processes to detect the error. That’s why we lost the spacecraft.”

—Dr. Edward Weiler, NASA Associate Administrator for Space Science [JPL99]

In fact, I don’t know anything about Mars Climate Orbiter’s system software. But according to the examination report of the failure, I’ve understood that one piece of software produced results in an “English system” unit, while the other piece of software that used those results expected them to be in metric units.

I think everybody knows C++ member function declarations that look like the one in the following class:

class SpacecraftTrajectoryControl { public:

void applyMomentumToSpacecraftBody(const double impulseValue);

};

What does the double stand for? Of what unit is the value that is expected by the member function named applyMomentumToSpacecraftBody? Is it a value measured in Newtons (N), newton-seconds (N·s), pound-force-seconds (lbf·s), or any other unit? In fact, we don’t know. The double can be anything. It is, of course, a type, but it is not a semantic type. Maybe it has been documented somewhere, or we could give the parameter a more meaningful and verbose name like impulseValueInNewtonSeconds, which would be better than nothing. But even the best documentation or parameter name cannot guarantee that a client of this class passes a value of an incorrect unit to this member function.

Can we do it better? Of course we can.

What we really want to have to define an interface properly, with rich semantics, is something like this:

class SpacecraftTrajectoryControl { public:

void applyMomentumToSpacecraftBody(const Momentum& impulseValue);

};

In mechanics, momentum is measured in newton-seconds (Ns). One newton-­ second (1 Ns) is the force of one Newton (which is 1 kg m/s2 in SI base units) acting on a body (a physical object) for one second.

173

Chapter 5 Advanced Concepts of Modern C++

To use a type like Momentum instead of the unspecific floating-point type double, we have to introduce that type first. In the first step we define a template that can be used to represent physical quantities on the base of the MKS system of units. The abbreviation MKS stands for meter (length), kilogram (mass), and seconds (time). These three fundamental units can be used to express many physical measurements. See Listing 5-20.

Listing 5-20.  A Class Template to Represent MKS Units

#include <type_traits>

template <int M, int K, int S> struct MksUnit {

enum { metre = M, kilogram = K, second = S};

};

You might wonder about why the Type Traits library (the <type_traits> header) is included on the first line? Well, type traits can be used to inspect the properties of types.

TYPE TRAITS [C++11]

Type traits can be regarded as one of the pillars of C++ template metaprogramming. When developers define a C++ template, the concrete types used to instantiate this template can theoretically be almost anything. For instance, when they define a class template like this:

template <typename T> class MyClassTemplate {

// ...

};

the template argument T can be substituted during instantiation with an int, a double, a std::string, or any other arbitrary data type that is defined by itself.

Using type traits, developers can let the compiler inspect which concrete data type is intended for the generic T during instantiation and can use the result of this check for conditional compiling. From a technical point of view, a type trait is a simple template struct, like this one:

template <typename T>

struct is_integral : bool_constant<> { // ...

};

174

Chapter 5 Advanced Concepts of Modern C++

This type trait checks whether T is an integral type (bool, char, int, unsigned int, ...). After its instantiation with a concrete data type for the template parameter T, the type trait holds a Boolean member constant, usually named value, containing the result of the check. This value can then be directly accessed (std::is_integral<T> ::value), but the more compact variant std::is_integral_v<T> is more common:

#include <type_traits>

template <typename T> class MyClassTemplate {

static_assert(std::is_integral_v<T> , "T must be an integral type!");

};

int main() {

MyClassTemplate<char8_t> foo; // OK!

MyClassTemplate<float> bar; // error: static assertion failed: T must be an integral type!

return 0;

}

Another category of type traits are those that alter the passed concrete type for template parameter T. For instance, the type trait std::remove_reference<T> transforms a reference type T& into T. The result of this transformation can be accessed through a member type alias usually named type.

In our case we need the Type Traits library to define a constraint with the help of C++ concepts. See Listing 5-21.

Listing 5-21.  A C++ Concept to Check Whether a Type Is an Instantiation of the MksUnit Template

template <typename T>

struct IsMksUnitType : std::false_type { };

template <int M, int K, int S>

struct IsMksUnitType<MksUnit<M, K, S>> : std::true_type { };

template <typename T>

concept MksUnitType = IsMksUnitType<T>::value;

175

Chapter 5 Advanced Concepts of Modern C++

STD::TRUE_TYPE AND STD::FALSE_TYPE (SINCE C++11)

Since C++11, there is a class template std::integral_constant (defined in the <type_traits> header) available that takes an integral type and an integral value as template parameters. Two type aliases, std::true_type and std::false_type, are also defined in the <type_traits> header for the common case where the template parameter T of std::integral_constant is of type bool. In simplified terms, they are defined like this:

using true_type = integral_constant<bool, true>; using false_type = integral_constant <bool, false>;

These two aliases are used to represent the Boolean values true and false as types and serve as the base classes for many type traits. They can be used for so-called tag dispatching, which is a technique to select an implementation of a function from a set of overloaded functions that suits a given type. Here is a small example:

#include <type_traits>

template <typename T>

auto calculateImpl(T value, std::true_type) { // Implementation for arithmetic value types

}

template <typename T>

auto calculateImpl(T value, std::false_type) {

// Implementation for non-arithmetic value types

}

template <typename T> auto calculate(T value) {

return calculateImpl(value, std::is_arithmetic<T>{});

}

Depending on whether the data type used to call the calculate() function is an arithmetic type (that is, an integral type or a floating-point type) or not, the appropriate calculateImpl() function template is selected at compile time.

176

Chapter 5 Advanced Concepts of Modern C++

With this concept (I discuss C++20 concepts in more detail later), we want to ensure under all circumstances that the template class Value presented in Listing 5-22 is always instantiated with a proper instantiated template class, MksUnit.

Listing 5-22.  A Class Template to Represent Values of MKS Units

template <typename T> requires MksUnitType<T> class Value {

public:

explicit Value(const long double magnitude) noexcept : magnitude(magnitude) {}

long double getMagnitude() const noexcept { return magnitude;

}

private:

long double magnitude{ 0.0 };

};

Next, we can use both class templates to define type aliases for concrete physical quantities. Here are some examples:

using DimensionlessQuantity = Value<MksUnit<0, 0, 0>>; using Length = Value<MksUnit<1, 0, 0>>;

using Area = Value<MksUnit<2, 0, 0>>; using Volume = Value<MksUnit<3, 0, 0>>; using Mass = Value<MksUnit<0, 1, 0>>; using Time = Value<MksUnit<0, 0, 1>>; using Speed = Value<MksUnit<1, 0, -1>>;

using Acceleration = Value<MksUnit<1, 0, -2>>; using Frequency = Value<MksUnit<0, 0, -1>>; using Force = Value<MksUnit<1, 1, -2>>;

using Pressure = Value<MksUnit<-1, 1, -2>>; // ... etc. ...

177

Chapter 5 Advanced Concepts of Modern C++

It is also possible to define the Momentum, which is required as the parameter type for our applyMomentumToSpacecraftBody member function:

using Momentum = Value<MksUnit<1, 1, -1>>;

After we’ve introduced the type alias Momentum, the following code will not compile, because there is no suitable constructor to convert from double to

Value<MksUnit<1,1,-1>>:

SpacecraftTrajectoryControl control; const double someValue = 13.75;

control.applyMomentumToSpacecraftBody(someValue); // Compile-time error!

The next example will also lead to compile-time errors, because a variable of type Force must not be used like a Momentum, and an implicit conversion between these different dimensions must be prevented:

SpacecraftTrajectoryControl control; Force force { 13.75 };

control.applyMomentumToSpacecraftBody(force); // Compile-time error!

But this will work fine:

SpacecraftTrajectoryControl control; Momentum momentum { 13.75 };

control.applyMomentumToSpacecraftBody(momentum);

The units can also be used to define constants. For this purpose, we need to slightly modify the class template Value. We add the keyword constexpr (see the section entitled “Computations During Compile Time” earlier in this chapter) to the initialization constructor and the getMagnitude() member function. This allows us to create compile-­ time constants of Value that don’t have to be initialized during runtime. As you will see later, we can also perform computations with our physical values during compile time now.

template <typename T> requires MksUnitType<T> class Value {

public:

constexpr explicit Value(const long double magnitude) noexcept : magnitude { magnitude } {}

178

Chapter 5 Advanced Concepts of Modern C++

constexpr long double getMagnitude() const noexcept { return magnitude;

}

private:

long double magnitude { 0.0 };

};

Thereafter, constants of different physical units can be defined, as in the following example:

constexpr Acceleration gravitationalAccelerationOnEarth { 9.80665 }; constexpr Pressure standardPressureOnSeaLevel { 1013.25 }; constexpr Speed speedOfLight { 299792458.0 };

constexpr Frequency concertPitchA { 440.0 }; constexpr Mass neutronMass { 1.6749286e-27 };

Furthermore, computations between units are possible if the necessary operators are implemented. For instance, these are the addition, subtraction, multiplication, and division operator templates that perform different calculations with two values of different MKS units:

template <int M, int K, int S>

constexpr Value<MksUnit<M, K, S>> operator+

(const Value<MksUnit<M, K, S>>& lhs, const Value<MksUnit<M, K, S>>& rhs) noexcept {

return Value<MksUnit<M, K, S>>(lhs.getMagnitude() + rhs.getMagnitude());

}

template <int M, int K, int S>

constexpr Value<MksUnit<M, K, S>> operator-

(const Value<MksUnit<M, K, S>>& lhs, const Value<MksUnit<M, K, S>>& rhs) noexcept {

return Value<MksUnit<M, K, S>>(lhs.getMagnitude() - rhs.getMagnitude());

}

template <int M1, int K1, int S1, int M2, int K2, int S2> constexpr Value<MksUnit<M1 + M2, K1 + K2, S1 + S2>> operator*

179

Chapter 5 Advanced Concepts of Modern C++

(const Value<MksUnit<M1, K1, S1>>& lhs, const Value<MksUnit<M2, K2, S2>>& rhs) noexcept {

return Value<MksUnit<M1 + M2, K1 + K2, S1 + S2>>(lhs.getMagnitude() * rhs.getMagnitude());

}

template <int M1, int K1, int S1, int M2, int K2, int S2> constexpr Value<MksUnit<M1 - M2, K1 - K2, S1 - S2>> operator/

(const Value<MksUnit<M1, K1, S1>>& lhs, const Value<MksUnit<M2, K2, S2>>& rhs) noexcept {

return Value<MksUnit<M1 - M2, K1 - K2, S1 - S2>>(lhs.getMagnitude() / rhs.getMagnitude());

}

Now you could write something like this:

constexpr Momentum impulseValueForCourseCorrection = Force { 30.0 } * Time { 3.0 };

SpacecraftTrajectoryControl control; control.applyMomentumToSpacecraftBody(impulseValueForCourseCorrection);

That’s obviously a significant improvement over a multiplication of two meaningless doubles and assigning the result to another meaningless double. It’s pretty expressive. And it’s safer, because you cannot assign the result of the multiplication to something different than a variable of type Momentum.

And the best part is this: the type safety is ensured during compile time! There is no overhead during runtime, because a C++11 (and higher)-compliant compiler can perform all the necessary type compatibility checks.

Let’s go one step further. Would it not be very convenient and intuitive if we could write something like the following?

constexpr Acceleration gravitationalAccelerationOnEarth { 9.80665_ms2 };

Even that is possible with modern C++. Since C++11, we can provide custom suffixes for literals by defining special functions—so-called literal operators—for them:

constexpr Force operator"" _N(long double magnitude) { return Force(magnitude);

}

180

Chapter 5 Advanced Concepts of Modern C++

constexpr Acceleration operator"" _ms2(long double magnitude) { return Acceleration(magnitude);

}

constexpr Time operator"" _s(long double magnitude) { return Time(magnitude);

}

constexpr Momentum operator"" _Ns(long double magnitude) { return Momentum(magnitude);

}

// ...more literal operators here...

USER-DEFINED LITERALS [C++11]

Basically, a literal is a compile-time constant whose value is specified in the source file. Since C++11, developers can produce objects of user-defined types by defining user-defined suffixes for literals. For instance, if a constant should be initialized with a literal of U.S. $145.67, this can be done by writing the following expression:

constexpr Money amount = 145.67_USD;

In this case, _USD is the user-defined suffix (Important: They must always begin with an underscore!) for floating-point literals that represent money amounts. So that a user-defined literal can be used, a function that is known as a literal operator must be defined:

constexpr Money operator"" _USD (const long double amount) { return Money(amount);

}

Once we’ve defined user-defined literals for our physical units, we can work with them in the following manner:

Force force = 30.0_N;

Time time = 3.0_s;

Momentum momentum = force * time;

181