- •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 5 Advanced Concepts of Modern C++
Concepts: Requirements for Template Arguments
The C++ template mechanism is a Turing Complete metalanguage for generic programming, which calculates types and values at compile time. There is nothing comparable in other programming languages, which only come close to the power of C++ templates.
On the downside, data type independent (generic) programming with templates is inherently complex and demanding. Just take a look at an outstanding example, the template code of the C++ Standard Library, and you know what I mean. You will be confronted with code that in many ways does not conform to the clean code guidelines I’ve presented in this book. On the contrary, it looks complex and cumbersome.
Many developers who write domain-specific application code are often very intensive users of template libraries, but they rarely come into a situation to write a template class or template function. But even as a user of templates, you often get into a situation where you have instantiated a template with one or more concrete data types for its template arguments, and were confronted with a very long and verbose list of cryptic error messages.
Just an example: earlier in this chapter, in the section about the <algorithm> header, I presented a small code example (see Listing 5-25) where a std::vector<T> filled with strings was sorted and then printed on stdout. In Listing 5-33, I modify this example a little bit by using a std::list<T> instead of a std::vector<T>.
Listing 5-33. Using a std::list Instead of a std::vector for names
#include <algorithm> #include <iostream>
#include <string>
#include <string_view>
#include <list> // formerly: <vector>
void printCommaSeparated(std::string_view text) { std::cout << text << ", ";
}
int main() {
std::list<std::string> names = { "Peter", "Harry", "Julia", "Marc", "Antonio", "Glenn" };
215
Chapter 5 Advanced Concepts of Modern C++
std::sort(begin(names), end(names)); std::for_each(begin(names), end(names), printCommaSeparated); return 0;
}
If you now compile this example, the compiler confronts you with a long list of sometimes hard-to-understand error messages. Then you are faced with the question: What the heck went wrong? I only exchanged the container type; can’t a std::list<T> be sorted?
The reason for that bunch of errors is that a std::list<T> only offers a bidirectional iterator; that is, an iterator that can be used to access the sequence of elements in both directions. However, the algorithm std::sort requires a random access iterator, i.e. an iterator that can be used to access elements at an arbitrary offset position relative to the element it points to.
The basic problem is that a template instantiation is first of all only an obtuse, textual replacement of the template arguments by concrete types. The compiler can only determine whether the template is at all suitable to work correctly with this type when it compiles the instantiated template. In addition, it is almost impossible to implement a function or class template in such a way that it fits every conceivable concrete data type.
With the new C++20 standard, template designers get a long-awaited feature: Concepts! Concepts are named sets of semantic requirements or constraints that can be applied on template parameters and are evaluated at compile-time. Thus, they become part of the template’s interface. We also get improved error messages, because the compiler can check if the requirements specified in a concept are satisfied by the concrete template arguments.
A C++ concept can be specified completely by yourself (I’ve done this in some code examples in this chapter before), but there is also a collection of predefined core concepts in the <concepts> header. These can be combined to build higher- level concepts. Furthermore, several concepts are also defined in other headers of the Standard Library, such as in <iterator> and <ranges>.
216
Chapter 5 Advanced Concepts of Modern C++
Specifying aConcept
Let’s assume that we want to develop a function template named function() whose one and only template parameter must be copyable. The corresponding C++ concept looks like this:
#include <concepts>
template<typename T> concept Copyable =
std::copy_constructible<T> && std::movable <T> && std::assignable_from<T&, const T&> && &&
std::assignable_from<T&, const T&> && std::assignable_from<T&, const T>;
Note The previous code snippet is for demonstration purposes only. It is not necessary to define a concept like Copyable by yourself, because it is included in the <concept>: std::copyable<T> header.
The specified requirement that a template parameter T should be copyable corresponds to a logical AND of five core concepts from the <concepts> header. Our new concept also got a good, semantic name: Copyable.
Another way to specify a concept is using the requires expression:
template<typename T>
concept Addable = requires (T x) { x + x; };
In this case, we have specified that a concrete type for template argument T can be added.
Applying aConcept
Now we apply the concept Copyable<T> by specifying the requirements for the template parameter T of a function, as shown in Listing 5-34.
217
Chapter 5 Advanced Concepts of Modern C++
Listing 5-34. Using a C++20 Concept to Specify Requirements That T Must Satisfy
class CopyableType { };
class NonCopyableType { public:
NonCopyableType() = default; NonCopyableType(const NonCopyableType&) = delete;
NonCopyableType& operator=(const NonCopyableType&) = delete;
};
template<typename T>
void function(T& t) requires Copyable<T> { // ...
};
int main() { CopyableType a; function(a); // OK! NonCopyableType b;
function(b); // Compiler error! return 0;
}
Because I deleted the copy constructor and copy assignment operator of the NonCopyableType class, we get the following expressive error message (excerpt; compiler: Clang 13.0.0):
prog.cc:28:3: error: no matching function for call to 'function' function(b); // Compiler error!
^~~~~~~~
prog.cc:20:6: note: candidate template ignored: constraints not satisfied [with T = NonCopyableType]
void function(T& t) requires Copyable<T> {
^
prog.cc:20:30: note: because 'NonCopyableType' does not satisfy 'Copyable' void function(T& t) requires Copyable<T> {
^
[...]
218
Chapter 5 Advanced Concepts of Modern C++
I highlighted the relevant line with bold font: The data type NonCopyableType does not satisfy the requirements of our concept named Copyable<T>. In the following lines of this error output (intentionally omitted here and replaced by an ellipsis: [...]), the compiler tells us which partial requirement of the concept was not satisfied. This is a significant improvement compared to those cryptic error messages from former times.
By the way, the function from Listing 5-34 can be written much more compact and elegant without the requires clause:
template<Copyable T> void function(T& t) {
// ...
};
Or even better, using the C++20 abbreviated function template syntax:
void function(Copyable auto& t) { // ...
};
Templates, concepts, and metaprogramming during compile time are extremely powerful features of modern C++ whose primary target group is clearly library developers. They justify a much more detailed introduction. Unfortunately, a deep dive into these language constructs is far beyond the scope of this book.
219
CHAPTER 6
Modularization
“I have absolutely no idea about space exploration. I’m a software guy. But because I’m a non-expert, I’ve been able to bring the software concept of modularity into the space sector, which was never done before.”
—Naveen Jain, software engineer, entrepreneur and founder, May 12, 2015
This quote is from a blog article [Jain15] by Naveen Jain, one of the three founders of the Florida-based private company Moon Express Inc., which was founded in 2010. The business objective of Moon Express (MoonEx) is to mine natural resources of economic value, such as ore, on the moon. For this purpose, MoonEx engineers designed a family of flexible and scalable robotic explorers based on modular spacecraft architecture. The foundation for their modular architecture is NASA’s Modular Common Spacecraft Bus (MCSB), which is a general-purpose spacecraft platform that can be configured as landers or orbiters. The MCSB not only reduces costs; NASA states that an uncrewed space mission that is built on the MCSB platform is roughly one-tenth the price of a conventional mission. Furthermore, by using a modular platform, NASA will no longer “reinvent the wheel,” by being able to reuse many components.
Since the early days of software development, developers strove for well-modularized software. The reason for this is obvious: Once a piece of software has reached a certain size, it gets more and more difficult for humans to grasp it in its entirety. We do not modularize for the computer. A computer doesn’t need a modularized version of the code to run it. It’s our own cognitive limitations that force us to break down a software system in smaller pieces.
In addition, people expect further positive effects from well modularized software: reusable modules, better maintainability, and easier extensibility. Creating a scalable, configurable, and flexible product family like MoonEx did with its robotic space probes is the goal. Furthermore, modules with minimal interdependencies and well-designed interfaces are easier to test.
221
© Stephan Roth 2021
S. Roth, Clean C++20, https://doi.org/10.1007/978-1-4842-5949-8_6