- •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++
is, reimplementing something (a library or a framework) that is available somewhere else. The reasoning behind this attitude is often the belief that in-house developments must be better in several respects. They are often mistakenly regarded as cheaper, more secure, more flexible, and more controllable than existing and well-established solutions.
In fact, only a few companies succeed in developing a truly equivalent, or even better alternative, to a solution that exists on the market. Often, the enormous effort of such developments does not justify the low benefit. And not infrequently is the self-developed library or framework clearly worse in quality compared to existing and mature solutions that have existed for years.
Over the past decades, many excellent libraries and frameworks have emerged in the C++ environment. These solutions had the chance to mature over a long time, and have been used successfully in tens of thousands of projects. There is no need to reinvent the wheel. Good software craftspeople should know about these libraries. It is not required to know every tiny detail about these libraries and their APIs. It is just good to know, however, that there are tried-and-tested solutions for certain fields of application, which are worth looking at to take into a narrower selection for your software development project.
Take Advantage of <algorithm>
“If you want to improve the code quality in your organization, replace all your coding guidelines with one goal: No raw loops!”
—Sean Parent, principal software architect with Adobe, at CppCon 2013
Fiddling with collections of elements is everyday business in programming. Regardless of whether we are dealing with collections of measurement data, with emails, strings, records from a database, or other elements, software must filter them, sort them, delete them, manipulate them, and more.
In many programs, we can find “raw loops” (e.g., hand-crafted for loops or while loops) for visiting some or all elements in a container, or sequence, in order to do something with them. A simple example is to reverse an order of integers stored in a std::vector this way:
#include <vector>
std::vector<int> integers { 2, 5, 8, 22, 45, 67, 99 };
183
Chapter 5 Advanced Concepts of Modern C++
// ...somewhere in the program: std::size_t leftIndex = 0;
std::size_t rightIndex = integers.size() - 1;
while (leftIndex < rightIndex) { int buffer = integers[rightIndex];
integers[rightIndex] = integers[leftIndex]; integers[leftIndex] = buffer;
++leftIndex; --rightIndex;
}
Basically this code will work. But it has several disadvantages. It is difficult to see immediately what this piece of code is doing (in fact, the first three lines inside the while loop could be substituted with std::swap from the <utility> header). Furthermore, writing code this way is very tedious and error prone. Just imagine that, for any reason, we violate the boundaries of the vector and try to access an element at a position out of range. Unlike member function std::vector::at(), std::vector::operator[] does not raise a std::out_of_range exception then. It will lead to undefined behavior.
The C++ Standard Library provides more than 100 useful algorithms that can be applied to containers or sequences for searching, counting, and manipulating elements. They are collected in the <algorithm> header.
For example, to reverse the order of elements in any kind of Standard Library container, for example, in a std::vector, we can simply use std::reverse:
#include <algorithm>
#include <vector>
std::vector<int> integers = { 2, 5, 8, 22, 45, 67, 99 };
//...somewhere in the program: std::reverse(begin(integers), end(integers));
//The content of 'integers' is now: 99, 67, 45, 22, 8, 5, 2
Unlike our self-written solution before, this code is much more compact, less error prone, and easier to read. Since std::reverse is a function template (like all other algorithms too), it is universally applicable to all Standard Library sequence containers,
184
Chapter 5 Advanced Concepts of Modern C++
associative containers, unordered associative containers, std::string, and primitive arrays (which, by the way, should not be used anymore in a modern C++ program; see the section “Prefer Standard Library Containers over Simple C-Style Arrays” in Chapter 4). See Listing 5-23.
Listing 5-23. Applying std::reverse to a C-Style Array and a String
#include <algorithm>
#include <string>
// Works, but primitive arrays should not be used in a modern C++ program int integers[] = { 2, 5, 8, 22, 45, 67, 99 }; std::reverse(begin(integers), end(integers));
std::string text { "The big brown fox jumps over the lazy dog!" }; std::reverse(begin(text), end(text));
// Content of 'text' is now: "!god yzal eht revo spmuj xof nworb gib ehT"
The reverse algorithm can be applied, of course, also to sub-ranges of a container or sequence, as shown in Listing 5-24.
Listing 5-24. Only a Sub-Area of the String Is Reversed
std::string text { "The big brown fox jumps over the lazy dog!" }; std::reverse(begin(text) + 13, end(text) - 9);
// Content of 'text' is now: "The big brown eht revo spmuj xof lazy dog!"
Easier Parallelization of Algorithms Since C++17
“Your free lunch will soon be over.”
—Herb Sutter [Sutter04]
The previous quote, which was addressed to software developers all over the world, is taken from an article published by Herb Sutter, member of the ISO C++ standardization committee, in 2004. It was at a time when the clock rates of processors stopped increasing from year to year. In other words, serial-processing speed has reached a physical limit. Instead, processors were increasingly equipped with more cores. This development in processor architectures leads to a heavy consequence: developers can
185
Chapter 5 Advanced Concepts of Modern C++
no longer take advantage of ever-increasing processor performance by clock rates—the “free lunch” that Herb was talking about—but they will be forced to develop massively multithreaded programs as a way to better utilize modern multi-core processors. As a result, developers and software architects now need to consider parallelization in their software architecture and design.
Before the advent of C++11, the C++ standard supported only single-threaded programming, and you have to use third-party libraries (e.g., Boost.Thread) or compiler extensions (e.g., Open Multi-Processing—OpenMP) to parallelize your programs. Since C++11, the Thread Support Library is available to support multithreaded and parallel programming. This extension of the Standard Library has introduced threads, mutual exclusions, condition variables, and futures.
Parallelizing a section of code requires good problem knowledge and must be considered in the software design accordingly. Otherwise, subtle errors caused by race conditions can occur that could be very difficult to debug. Especially for the algorithms of the Standard Library, which often have to operate on containers that are filled with a huge number of objects, the parallelization should be simplified for developers in order to exploit today’s modern multi-core processors.
Starting with C++17, parts of the Standard Library have been redesigned according to the Technical Specification for C++ Extensions for Parallelism (ISO/IEC TS 19570:2015), also known as the Parallelism TS (TS stands for technical specification). In other words, with C++17 these extensions became part of the mainline ISO C++ standard. Their main goal is to relieve developers a bit from the complex task of fiddling around with those low-level language features from the Thread Support Library, such as std::thread, std::mutex, etc.
In fact that means that about 70 well-known algorithms were overloaded and are now also available in one or more versions accepting an extra template parameter for parallelization called ExecutionPolicy. Some of these algorithms are, for instance, std::for_each, std::transform, std::copy_if, or std::sort. Furthermore, seven new algorithms have been added that can also be parallelized, like std::reduce, std::exclusive_scan, or std::transform_reduce. These new algorithms are particularly useful in functional programming, which is why I discuss them in Chapter 7.
186
Chapter 5 Advanced Concepts of Modern C++
EXECUTION POLICIES [C++17/C++20]
With the appearance of the standard C++17, a majority of algorithm templates from the <algorithm> header have been overloaded and are also available in a parallelizable version. For example, in addition to the existing template for the std::find function, another version has been defined that takes an additional template parameter to specify the execution policy:
//Standard (single-threaded) version: template< class InputIt, class T >
constexpr InputIt find( InputIt first, InputIt last, const T& value );
//Additional version with user-definable execution policy (since C++17): template< class ExecutionPolicy, class ForwardIt, class T >
ForwardIt find(ExecutionPolicy&& policy, ForwardIt first, ForwardIt last, const T& value);
The four standard policy tags that are available in C++20 for the template parameter
ExecutionPolicy are:
•\ std::execution::seq (since C++17). An execution policy type that defines that a parallel algorithm’s execution may be sequentially. Hence, it is more or less the same as you would use the single-threaded standard version of the algorithm template function without an execution policy.
•\ std::execution::par (since C++17.) An execution policy type that defines that a parallel algorithm’s execution may be parallelized. It permits the implementation to execute the algorithm on multiple threads. Important: The parallel algorithms do not automatically protect against critical data races or deadlocks! You are responsible for ensuring that no data race conditions can occur while executing the function.
•\ std::execution::par_unseq (since C++17). An execution policy type that defines that a parallel algorithm’s execution may be vectorized, parallelized, or migrated across threads. Vectorization takes advantage of the SIMD (Single Instruction, Multiple Data) command set of modern CPUs. SIMD means
that a processor can perform the same operation on multiple data points simultaneously.
187