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

Chapter 5 Advanced Concepts of Modern C++

createMonthlyInvoicesController will be. By the way, repeating the explicit type would also be a kind of violation of the DRY principle (see Chapter 3). And if you think of the lambda expression named square, whose type is a unique, unnamed non-union class type, how can such a type be explicitly defined?

Tip  If it doesn’t obscure the intent of your code, use auto wherever possible!

Computations During Compile Time

Fans of high-performance computing (HPC)—as well as developers of embedded software and programmers who prefer to use static, constant tables to separate data and code—want to compute as much as possible at compile time. The reasons for this are very easy to comprehend: everything that can be computed or evaluated at compile time does not have to be computed or evaluated at runtime. In other words, the computation of as much as possible at compile time is low-hanging fruit to raise the runtime efficiency of your program. This advantage is sometimes accompanied by a drawback, which is the more or less increasing time that it takes to compile the code.

Since C++11, the constexpr (constant expression) specifier makes it possible to evaluate the value of a function or a variable at compile time. With the subsequent standard C++14, some of the stringent restrictions for constexpr were lifted. For instance, a constexpr-specified function was allowed to have exactly one return statement only. This restriction has been abolished since C++14.

One of the simplest examples is that a variable’s value is calculated from literals by arithmetic operations at compile time, like this:

constexpr int theAnswerToAllQuestions = 10 + 20 + 12;

The theAnswerToAllQuestions variable is also a constant if it was declared with const; thus, you cannot manipulate it during runtime:

int main() { // ...

theAnswerToAllQuestions = 23; // Compiler error: assignment of read-only variable!

return 0;

}

164

Chapter 5 Advanced Concepts of Modern C++

There are also constexpr functions:

constexpr int multiply(const int multiplier, const int multiplicand) { return multiplier * multiplicand;

}

Such functions can be called at compile time, but they can also be used like ordinary functions with non-const arguments at runtime. This is necessary to test those functions with the help of unit tests (see Chapter 2).

constexpr int theAnswerToAllQuestions = multiply(7, 6);

Unsurprisingly, constexpr specified functions can also be called recursively, as shown in the example in Listing 5-16, which shows a function that calculates factorials.

Listing 5-16.  Calculating the Factorial of a Non-Negative Integer ‘n’ at Compile Time

01 #include <iostream>

02

03 constexpr unsigned long long factorial(const unsigned short n) { 04 return n > 1 ? n * factorial(n - 1) : 1;

05 }

06

07 int main() {

08 unsigned short number = 6;

09auto result1 = factorial(number);

10constexpr auto result2 = factorial(10);

12std::cout << "result1: " << result1 << ", result2: " << result2 << std::endl;

13return 0;

14}

The previous example works under C++11. The factorial() function consists of only one statement, and recursion was allowed from the beginning in constexpr functions. The main() function contains two calls of the factorial() function. It is worth it to take a closer look at these two function calls.

165

Chapter 5 Advanced Concepts of Modern C++

The first call on line 9 uses the variable number as the argument for the function’s parameter n, and its result is assigned to a non-const variable result1. The second function call on line 10 uses a number literal as the argument, and its result is assigned to a variable with a constexpr specifier. The difference between these two function calls at runtime can best be seen in the disassembled object code. Figure 5-3 shows the object code at the key spot in the Disassembly window of Eclipse CDT.

Figure 5-3.  The disassembled object code

The first function call on line 9 results in five machine instructions. The fourth of these instructions (callq) is the jump to the function factorial() at memory address 0x5555555549bd. In other words, it is obvious that the function is called at runtime. In contrast, we see that the second call of factorial() at line 10 results in just one simple machine instruction. The movq instruction copies a quadword from the source operand to the destination operand. There is no costly function call at runtime. The result of factorial(10), which is 0x375f00 in hexadecimal and 3,628,800 in decimal, has been calculated at compile time and is available like a constant in the object code.

As I mentioned earlier, some restrictions for contexpr specified functions in C++11 have been repealed since C++14. For instance, a constexpr specified function can now have more than one return statement; it can have conditionals like if-else-branches, local variables of “literal” type, or loops. Basically, almost all C++ statements are allowed if they do not presuppose or require something that is only available in the context

of a runtime environment, for example, allocating memory on the heap, or throwing exceptions.

166

Chapter 5 Advanced Concepts of Modern C++

Variable Templates

I think it is less surprising that constexpr can also be used in templates, as shown in the example in Listing 5-17.

Listing 5-17.  A Variable Template for the Mathematical Constant pi

#include <concepts>

template<typename T>

concept FloatingPoint = std::floating_point<T>;

template <typename T> requires FloatingPoint<T> constexpr T pi = T(3.1415926535897932384626433L);

For the moment, we ignore the first lines of code in Listing 5-17 and focus on the last two lines only. What we can see there is known as a variable template. It is a good and flexible alternative to the archaic style of constant definitions using #define macros (see the section entitled “Avoid Macros” in Chapter 4). Depending on its usage context during template instantiation, the mathematical constant pi is typed as float, double, or long double. See Listing 5-18.

Listing 5-18.  Calculating a Circle’s Circumference at Compile Time Using the Variable Template pi

template <typename T>

constexpr T computeCircumference(const T radius) requires FloatingPoint<T>

{

return 2 * radius * pi<T>;

}

int main() {

constexpr long double radius { 10.0L };

constexpr long double circumference = computeCircumference(radius); std::cout << circumference << std::endl;

return 0;

}

Okay, but what do the other lines of code before the variable template pi in

Listing 5-17 mean? Well, I’ve used a new and long-awaited feature of the C++20 language

167

Chapter 5 Advanced Concepts of Modern C++

standard called concepts. Concepts are an extension to the C++ template mechanism that define requirements or constraints for template parameters. In this case I’ve defined a concept to enforce users of the variable template pi as well as the function template computeCircumference to instantiate both with a floating-point data type, otherwise the compiler will report an error. I will give a bit more detailed insight into C++20 concepts later in this chapter.

Last but not least, it is noteworthy that you can also use classes in computations at compile time. You can define constexpr constructors and member functions for classes. See Listing 5-19.

Listing 5-19.  Rectangle Is a constexpr Class

#include <cmath>

#include <iostream>

class Rectangle { public:

constexpr Rectangle() = delete;

constexpr Rectangle(const double width, const double height) : width { width }, height { height } { }

constexpr double getWidth() const { return width; } constexpr double getHeight() const { return height; } constexpr double getArea() const { return width * height; } constexpr double getLengthOfDiagonal() const {

return std::sqrt(std::pow(width, 2.0) + std::pow(height, 2.0));

}

private: double width;

double height;

};

int main() {

constexpr Rectangle americanFootballPlayingField { 48.76, 110.0 }; constexpr double area = americanFootballPlayingField.getArea();

constexpr double diagonal = americanFootballPlayingField.getLengthOfDiagonal();

168