- •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 7 Functional Programming
Listing 7-4. A GCD Function Using Recursion That Can Be Evaluated at Compile Time
constexpr unsigned int greatestCommonDivisor(const unsigned int x,
const unsigned int y) noexcept
{
return y == 0 ? x : greatestCommonDivisor(y, x % y);
}
By the way, the mathematical algorithm behind this is called Euclidean algorithm, or Euclid’s algorithm, named after the ancient Greek mathematician Euclid.
And since C++17, the numeric algorithm std::gcd() has become part of the C++ Standard Library (defined in the <numeric> header), so it is not necessary to implement it on your own. See Listing 7-5.
Listing 7-5. Using the std::gcd Function from the <numeric> Header
#include <iostream> #include <numeric>
int main() {
constexpr auto result = std::gcd(40, 10);
std::cout << "The GCD of 40 and 10 is: " << result << std::endl; return 0;
}
Function-Like Objects (Functors)
What was also always possible in C++ from the very beginning was the definition and use of so-called function-like objects, also known as functors (another synonym is functionals) in short. Technically speaking, a functor is more or less just a class that defines the parenthesis operator, that is, the operator(). After the instantiation of these classes, they can pretty much be used like functions.
Depending on whether the operator() has none, one, or two parameters, the functor is called a generator, unary function, or binary function. Let’s look at a generator first.
302
Chapter 7 Functional Programming
Generator
As the name “generator” reveals, this type of functor is used to produce something. See Listing 7-6.
Listing 7-6. An Example of a Generator, a Functor That Is Called With No Argument
class IncreasingNumberGenerator { public:
[[nodiscard]] int operator()() noexcept { return number++; }
private:
int number { 0 };
};
The working principle is quite simple: every time IncreasingNumberGenerator: :operator() is called, the actual value of the member variable number is returned to the caller, and the value of this member variable is increased by 1. The following usage example prints a sequence of the numbers 0 to 2 on standard output:
int main() {
IncreasingNumberGenerator numberGenerator { }; std::cout << numberGenerator() << std::endl; std::cout << numberGenerator() << std::endl; std::cout << numberGenerator() << std::endl; return 0;
}
Remember the quote from Sean Parent that I presented in the section on algorithms in Chapter 5: no raw loops! To fill a std::vector<T> with a certain amount of increasing values, we should not implement a handcrafted loop. Instead, we can use std::generate or std::ranges::generate (since C++20), both defined in the <algorithm> header. Both are function templates that assign each element in a certain range a value generated by a given generator object. Hence, we can write the simple and well-readable code shown in Listing 7-7 to fill a vector with an increasing number sequence using IncreasingNumberGenerator.
303
Chapter 7 Functional Programming
Listing 7-7. Filling a Vector with an Increasing Number Sequence Using std::ranges::generate
#include <algorithm>
#include <vector>
using Numbers = std::vector<int>;
int main() {
const std::size_t AMOUNT_OF_NUMBERS { 100 }; Numbers numbers(AMOUNT_OF_NUMBERS);
std::ranges::generate(numbers, IncreasingNumberGenerator()); // ...now 'numbers' contains values from 0 to 99...
return 0;
}
As one can easily imagine, these kinds of functors do not fulfill the strict requirements for pure functions. Generators do commonly have a mutable state, that is, when operator() is called, these functors usually have some side effect. In our case, the mutable state is represented by the private member variable called IncreasingNumberGe nerator::number, which is incremented after each call of the parenthesis operator.
STD::IOTA (SINCE C++11) AND STD::RANGES::IOTA_VIEW (SINCE C++20)
Since C++11, the <numeric> header has contained a function template called std::iota(), named after the functional symbol (Iota) from the programming language APL. It’s not a generator functor, but it can be used to fill a container with an ascending sequence of values in an elegant way. Since C++20, this function template is also specified as constexpr and thus usable in compile-time computations.
Thus, the line from the previous code example where the vector is filled can also be written as follows:
std::iota(begin(numbers), end(numbers), 0);
304
Chapter 7 Functional Programming
With the introduction of the Ranges library since C++20, there is another way to generate a sequence of elements by repeatedly incrementing an initial value: the Range factory std::ranges::iota_view (defined in the <ranges> header):
auto view = std::ranges::iota_view { 0, 100 }; std::vector<int> numbers(std::begin(view), std::end(view)); // ...now 'numbers' contains values from 0 to 99...
Another example of a function-like object of type generator is the random number generator functor class template shown in Listing 7-8. This functor encapsulates all the stuff that is necessary for the initialization and usage of a pseudorandom number generator (PRNG) based on the so-called Mersenne Twister algorithm (defined in the
<random> header).
Listing 7-8. A Generator Functor Class Template Encapsulating a Pseudorandom Number Generator
#include <random>
template <typename NUMTYPE> class RandomNumberGenerator { public:
RandomNumberGenerator() { mersenneTwisterEngine.seed(randomDevice());
}
[[nodiscard]] NUMTYPE operator()() {
return distribution(mersenneTwisterEngine);
}
private:
std::random_device randomDevice; std::uniform_int_distribution<NUMTYPE> distribution; std::mt19937_64 mersenneTwisterEngine;
};
Listing 7-9 shows how the RandomNumberGenerator functor could then be used.
305