- •Table of Contents
- •About the Author
- •About the Technical Reviewer
- •Acknowledgments
- •Software Entropy
- •Clean Code
- •C++11: The Beginning of a New Era
- •Who This Book Is For
- •Conventions Used in This Book
- •Sidebars
- •Notes, Tips, and Warnings
- •Code Samples
- •Coding Style
- •C++ Core Guidelines
- •Companion Website and Source Code Repository
- •UML Diagrams
- •The Need for Testing
- •Unit Tests
- •What About QA?
- •Rules for Good Unit Tests
- •Test Code Quality
- •Unit Test Naming
- •Unit Test Independence
- •One Assertion per Test
- •Independent Initialization of Unit Test Environments
- •Exclude Getters and Setters
- •Exclude Third-Party Code
- •Exclude External Systems
- •What Do We Do with the Database?
- •Don’t Mix Test Code with Production Code
- •Tests Must Run Fast
- •How Do You Find a Test’s Input Data?
- •Equivalence Partitioning
- •Boundary Value Analysis
- •Test Doubles (Fake Objects)
- •What Is a Principle?
- •KISS
- •YAGNI
- •It’s About Knowledge!
- •Building Abstractions Is Sometimes Hard
- •Information Hiding
- •Strong Cohesion
- •Loose Coupling
- •Be Careful with Optimizations
- •Principle of Least Astonishment (PLA)
- •The Boy Scout Rule
- •Collective Code Ownership
- •Good Names
- •Names Should Be Self-Explanatory
- •Use Names from the Domain
- •Choose Names at an Appropriate Level of Abstraction
- •Avoid Redundancy When Choosing a Name
- •Avoid Cryptic Abbreviations
- •Avoid Hungarian Notation and Prefixes
- •Avoid Using the Same Name for Different Purposes
- •Comments
- •Let the Code Tell the Story
- •Do Not Comment Obvious Things
- •Don’t Disable Code with Comments
- •Don’t Write Block Comments
- •Don’t Use Comments to Substitute Version Control
- •The Rare Cases Where Comments Are Useful
- •Documentation Generation from Source Code
- •Functions
- •One Thing, No More!
- •Let Them Be Small
- •“But the Call Time Overhead!”
- •Function Naming
- •Use Intention-Revealing Names
- •Parameters and Return Values
- •Avoid Flag Parameters
- •Avoid Output Parameters
- •Don’t Pass or Return 0 (NULL, nullptr)
- •Strategies for Avoiding Regular Pointers
- •Choose simple object construction on the stack instead of on the heap
- •In a function’s argument list, use (const) references instead of pointers
- •If it is inevitable to deal with a pointer to a resource, use a smart one
- •If an API returns a raw pointer...
- •The Power of const Correctness
- •About Old C-Style in C++ Projects
- •Choose C++ Strings and Streams over Old C-Style char*
- •Use C++ Casts Instead of Old C-Style Casts
- •Avoid Macros
- •Managing Resources
- •Resource Acquisition Is Initialization (RAII)
- •Smart Pointers
- •Unique Ownership with std::unique_ptr<T>
- •Shared Ownership with std::shared_ptr<T>
- •No Ownership, but Secure Access with std::weak_ptr<T>
- •Atomic Smart Pointers
- •Avoid Explicit New and Delete
- •Managing Proprietary Resources
- •We Like to Move It
- •What Are Move Semantics?
- •The Matter with Those lvalues and rvalues
- •rvalue References
- •Don’t Enforce Move Everywhere
- •The Rule of Zero
- •The Compiler Is Your Colleague
- •Automatic Type Deduction
- •Computations During Compile Time
- •Variable Templates
- •Don’t Allow Undefined Behavior
- •Type-Rich Programming
- •Know Your Libraries
- •Take Advantage of <algorithm>
- •Easier Parallelization of Algorithms Since C++17
- •Sorting and Output of a Container
- •More Convenience with Ranges
- •Non-Owning Ranges with Views
- •Comparing Two Sequences
- •Take Advantage of Boost
- •More Libraries That You Should Know About
- •Proper Exception and Error Handling
- •Prevention Is Better Than Aftercare
- •No Exception Safety
- •Basic Exception Safety
- •Strong Exception Safety
- •The No-Throw Guarantee
- •An Exception Is an Exception, Literally!
- •If You Can’t Recover, Get Out Quickly
- •Define User-Specific Exception Types
- •Throw by Value, Catch by const Reference
- •Pay Attention to the Correct Order of Catch Clauses
- •Interface Design
- •Attributes
- •noreturn (since C++11)
- •deprecated (since C++14)
- •nodiscard (since C++17)
- •maybe_unused (since C++17)
- •Concepts: Requirements for Template Arguments
- •The Basics of Modularization
- •Criteria for Finding Modules
- •Focus on the Domain of Your Software
- •Abstraction
- •Choose a Hierarchical Decomposition
- •Single Responsibility Principle (SRP)
- •Single Level of Abstraction (SLA)
- •The Whole Enchilada
- •Object-Orientation
- •Object-Oriented Thinking
- •Principles for Good Class Design
- •Keep Classes Small
- •Open-Closed Principle (OCP)
- •A Short Comparison of Type Erasure Techniques
- •Liskov Substitution Principle (LSP)
- •The Square-Rectangle Dilemma
- •Favor Composition over Inheritance
- •Interface Segregation Principle (ISP)
- •Acyclic Dependency Principle
- •Dependency Inversion Principle (DIP)
- •Don’t Talk to Strangers (The Law of Demeter)
- •Avoid Anemic Classes
- •Tell, Don’t Ask!
- •Avoid Static Class Members
- •Modules
- •The Drawbacks of #include
- •Three Options for Using Modules
- •Include Translation
- •Header Importation
- •Module Importation
- •Separating Interface and Implementation
- •The Impact of Modules
- •What Is Functional Programming?
- •What Is a Function?
- •Pure vs Impure Functions
- •Functional Programming in Modern C++
- •Functional Programming with C++ Templates
- •Function-Like Objects (Functors)
- •Generator
- •Unary Function
- •Predicate
- •Binary Functors
- •Binders and Function Wrappers
- •Lambda Expressions
- •Generic Lambda Expressions (C++14)
- •Lambda Templates (C++20)
- •Higher-Order Functions
- •Map, Filter, and Reduce
- •Filter
- •Reduce (Fold)
- •Fold Expressions in C++17
- •Pipelining with Range Adaptors (C++20)
- •Clean Code in Functional Programming
- •The Drawbacks of Plain Old Unit Testing (POUT)
- •Test-Driven Development as a Game Changer
- •The Workflow of TDD
- •TDD by Example: The Roman Numerals Code Kata
- •Preparations
- •The First Test
- •The Second Test
- •The Third Test and the Tidying Afterward
- •More Sophisticated Tests with a Custom Assertion
- •It’s Time to Clean Up Again
- •Approaching the Finish Line
- •Done!
- •The Advantages of TDD
- •When We Should Not Use TDD
- •TDD Is Not a Replacement for Code Reviews
- •Design Principles vs Design Patterns
- •Some Patterns and When to Use Them
- •Dependency Injection (DI)
- •The Singleton Anti-Pattern
- •Dependency Injection to the Rescue
- •Adapter
- •Strategy
- •Command
- •Command Processor
- •Composite
- •Observer
- •Factories
- •Simple Factory
- •Facade
- •The Money Class
- •Special Case Object (Null Object)
- •What Is an Idiom?
- •Some Useful C++ Idioms
- •The Power of Immutability
- •Substitution Failure Is Not an Error (SFINAE)
- •The Copy-and-Swap Idiom
- •Pointer to Implementation (PIMPL)
- •Structural Modeling
- •Component
- •Interface
- •Association
- •Generalization
- •Dependency
- •Template and Template Binding
- •Behavioral Modeling
- •Activity Diagram
- •Action
- •Control Flow Edge
- •Other Activity Nodes
- •Sequence Diagram
- •Lifeline
- •Message
- •State Diagram
- •State
- •Transitions
- •External Transitions
- •Internal Transitions
- •Trigger
- •Stereotypes
- •Bibliography
- •Index
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