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

Chapter 6 Modularization

was successful and all automated tests ran without errors. Hence parallelization is the be-all and end-all here.

The fact that there are strict sequential processing steps when using C++ modules makes parallelization more difficult. Especially with more complex import graphs with a high DAG2-depth, i.e. with a long chain of modules that import each other, the potential to speed up compilation through parallelization can decrease significantly. Rene Rivera, who contributed to the famous Boost libraries, has carried out studies on the influence of the use of modules on the compiler performance, especially under different degrees of parallelization. He comes to the following conclusion:

“With the limitations of the capabilities of current compilers one can only conclude that modular builds are very advantageous at lower parallelism levels environments. But that it’s unclear if they are an advantage in highly parallel build environments. In other words, that modules currently do not scale in the same ways as traditional compilation.”

—Rene Rivera, “Are Modules Fast?” [Rivera19]

Three Options for Using Modules

Migration to C++ modules should also be easily possible in ongoing projects. It would be a big hurdle if there were no transition stages between the old concept of including header files on the one hand and importing modules on the other. For this reason, the new C++20 language standard provides three importing options, which I introduce briefly now.

Include Translation

The easiest step toward C++ modules in ongoing projects is to use the (header) include translation. Basically, include translation means treat the header includes like module imports. If certain constraints are fulfilled, especially that the header is importable, nothing in the code has to be adapted or changed, neither on the client’s side nor on the supplier’s side. However, it is important to point out that include translation is a compiler-dependent feature.

2Directed Acyclic Graph; a finite directed graph with no cycles

286

Chapter 6 Modularization

WHEN IS A HEADER FILE IMPORTABLE?

A header file that is suitable for both include translation and header importation must be sufficiently self-contained, i.e. it must be modular in a way so that it does not rely on pre-­ definitions, like macros or declarations, or post-undefinitions (macros).

For example, an include directive like #include <iostream> will be automatically mapped to an import of that header. Fortunately, as specified by the C++20 standard, compiler vendors have to provide their Standard Library headers in an importable format. In contrast, all C++ wrappers for C Standard Libraries, for instance <cstdio>, <cmath>, or <cstdlib>, will not be importable. But this should not bother us as clean code developers, because most of the content of these libraries should not be used in a modern C++ program anyway.

The C++20 include translation solves a couple of issues that we still had with the old-­ fashioned header inclusion. First, the translation speed is increased. In addition, some ODR violations are also prevented, since identical definitions in different header files no longer cause conflicts. Header files can no longer manipulate other header files, nor can the importing translation unit change the code of imported header files.

Header Importation

The next step toward C++ modules is header importation, sometimes also called header units, which requires a few minor changes in the code on the client’s side, i.e. the consumer of the module. These changes are very simple: replace each header include with an explicit import of that header. In other words, replace the #include directive with the new import keyword, as shown in Listing 6-35.

Listing 6-35.  Header Importation Example

import <iostream>; // ...instead of #include <iostream>

int main() {

std::cout << "Header Importation" << '\n'; return 0;

}

287

Chapter 6 Modularization

The advantages you get with header importation are basically the same as with include translation, explained in the previous section.

Module Importation

The highest level of using C++ modules is of course module importation, i.e. using modules designated for a modern C++ program. At this stage there are ideally no header files anymore, but the whole software is built of translation units and imported modules.

In Listing 6-36, you can find an example of a simple module, a small library of financial mathematical functions, which currently contains only one function.

Listing 6-36.  A Simple Module That Provides Just One Function

module;

#include <cmath>

export module financialmath;

namespace financialmath {

export long double calculateCompoundedInterest(const long double initialCapital,

const long double rate, const unsigned short term) {

return initialCapital * pow((1.0 + rate / 100.0), term);

}

}

The first thing you may notice is that the usual boilerplate code that is typical for header files, such as the include guard or a #pragma once statement, is gone. Instead, we find the beginning of the so-called global module fragment in the first line. The content of this area is not exported and is only visible within the module. For example, preprocessor instructions can be placed here (e.g., #include directives). In our simple case, we only include <cmath> here. The following export keyword followed by the module’s name introduces the module declaration. It declares and exposes the primary interface of a module named financialmath. Inside of the financialmath namespace, we see a function called calculateCompoundedInterest, which performs a compound interest calculation for a given initial capital at a given interest rate and a given term in years.

288

Chapter 6 Modularization

It is noteworthy that the function is preceded by an export keyword. Using this keyword enables a module developer to determine which parts of a module can be accessed from outside, e.g. by consumers, and which cannot. So we see another

enormous advantage that we get with modules: better support of the information hiding principle, which we learned about in Chapter 3.

The use of the module is demonstrated in the unit test in Listing 6-37, which tests the exported function.

Listing 6-37.  Calling the Exported Function in a Unit Test

import financialmath;

TEST(FinancialmathModuleTest, FinalCapitalIsCalculatedCorrectly) { const auto finalCapital = financialmath::calculateCompoundedInterest(

3500.0, 4.0, 3); EXPECT_DOUBLE_EQ(3937.024, finalCapital);

}

Module importation offers a number of additional advantages to those previously mentioned with Header translation and header importation. It is particularly noticeable that the separation between header files and source files no longer exists. Everything is located in a single module file (which also has some drawbacks, I’ll get right on that). Furthermore, the ordering of the import statements of modules doesn’t matter any more, because the consumer can import them in an arbitrary sequence. Cyclical imports are not possible. ODR violations are virtually a thing of the past.

Separating Interface and Implementation

As I just implied, it is not always an advantage if the module is only one single file. Especially if the module becomes very complex, it can be helpful to separate the module’s interface from its implementation, because then the module interface file remains clean without any implementation details.

Therefore, even with modules, there is the possibility to separate the usually stable public interface of the module, the Module Interface Unit, from the probably more frequently changed module implementation, the Module Implementation Unit. Our small financialmath module would be divided into two units, as shown in Listings 6-38 and 6-39.

289