- •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++
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