- •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
Map, Filter, and Reduce
Each serious functional programming language must provide at least three useful higher-order functions: map, filter, and reduce (called fold). Even if they have different names depending on the programming language, you can find this triumvirate in Haskell, Erlang, Clojure, JavaScript, Scala, and many other languages with functional programming capabilities. Hence, we can claim justifiably that these three higher-order functions form a very common functional programming design pattern.
It should therefore hardly surprise you that these higher-order functions are also contained in the C++ Standard Library. And maybe you will also not be surprised that we have already used some of these functions.
Let’s look at each of these functions in the following sections.
Map
Map might be the easiest to understand of the three. With the help of this higher-order function, we can apply an operator function to each single element of a list. In C++, this function is provided by the Standard Library algorithm std::transform (defined in the <algorithm> header), which you’ve seen in some previous code examples.
Filter
Filter is also pretty simply. As the name suggests, this higher-order function takes a predicate (see the section about predicates earlier in this chapter) and a list, and it removes any element from the list that does not satisfy the predicate’s condition. In C++, this function is provided by the Standard Library algorithm std::remove_if (defined in the <algorithm> header), which you’ve seen in some previous code examples.
Nevertheless, here’s another nice example of the std::remove_if filter. If you are suffering from a disease called “aibohphobia,” which is a humorous term for the
irrational fear of palindromes, you could filter out palindromes from word lists, as shown in Listing 7-25.
Listing 7-25. Removing All Palindromes from a Vector of Words
#include <algorithm> #include <iostream>
#include <string>
324
Chapter 7 Functional Programming
#include <vector>
class IsPalindrome { public:
bool operator()(const std::string& word) const {
const auto middleOfWord = begin(word) + word.size() / 2; return std::equal(begin(word), middleOfWord, rbegin(word));
}
};
int main() {
std::vector<std::string> someWords { "dad", "hello", "radar", "vector", "deleveled", "foo", "bar", "racecar", "ROTOR", "", "C++", "aibohphobia" }; someWords.erase(std::remove_if(begin(someWords), end(someWords), IsPalindrome()),
end(someWords));
std::for_each(begin(someWords), end(someWords), [](const auto& word) { std::cout << word << ",";
}); return 0;
}
The output of this program is as follows:
hello,vector,foo,bar,C++,
Reduce (Fold)
Reduce (also called fold, collapse, or aggregate) is the most powerful of the three higher- order functions and might be a bit hard to understand at first glance. Reduce (fold) is
a higher-order function to get a single resultant value by applying a binary operator to a list of values. In C++, this function is provided by the Standard Library algorithm
std::accumulate (defined in the <numeric> header). Some say that std::accumulate is the most powerful algorithm in the Standard Library.
To start with a simple example, you can easily get the sum of all integers in a vector, as shown in Listing 7-26.
325
Chapter 7 Functional Programming
Listing 7-26. Building the Sum of All Values in a Vector Using std::accumulate
#include <numeric> #include <iostream>
#include <vector>
int main() {
std::vector<int> numbers { 12, 45, -102, 33, 78, -8, 100, 2017, -110 };
const int sum = std::accumulate(begin(numbers), end(numbers), 0); std::cout << "The sum is: " << sum << std::endl;
return 0;
}
The version of std::accumulate we used does not expect an explicit binary operator in the parameter list. Using this version of the function, the sum of all values is calculated. Of course, you can provide your own binary operator, as in the example through a lambda expression in Listing 7-27.
Listing 7-27. Finding the Highest Number in a Vector Using std::accumulate
int main() {
std::vector<int> numbers { 12, 45, -102, 33, 78, -8, 100, 2017, -110 };
const int maxValue = std::accumulate(begin(numbers), end(numbers), 0, [](const int value1, const int value2) {
return value1 > value2 ? value1 : value2; });
std::cout << "The highest number is: " << maxValue << std::endl; return 0;
}
326
Chapter 7 Functional Programming
LEFT AND RIGHT FOLD
Functional programming often distinguishes between two ways to fold a list of elements: a left fold and a right fold.
If we combine the first element with the result of recursively combining the rest, this is called a right fold. Instead, if we combine the result of recursively combining all elements but the last one with the last element, this operation is called a left fold.
If, for example, we take a list of values that are to be folded with a + operator to a sum, the parentheses are as follows for a left fold operation: ((A + B) + C) + D. Instead, with a right fold, the parentheses would be set like this: A + (B + (C + D)). In the case of a simple associative + operation, the result does not change whether it is formed with a left fold or a right fold. But in the case of non-associative binary functions, the order in which the elements are combined may influence the final result’s value.
Also in C++, we can distinguish between a left fold and a right fold. If we use std::accumulate with normal iterators, we get a left fold:
std::accumulate(begin, end, init_value, binary_operator)
Instead, if we use std::accumulate with a reverse iterator, we get a right fold:
std::accumulate(rbegin, rend, init_value, binary_operator)
Fold Expressions in C++17
Starting with C++17, the language has gained an interesting new feature called fold expressions. Fold expressions are implemented as so-called variadic templates (available since C++11); that is, as templates that can take a variable number of arguments in a type-safe way. This arbitrary number of arguments is held in a so-called parameter pack.
What has been added with C++17 is the possibility to reduce the parameter pack directly with the help of a binary operator, that is, to perform a folding. The general syntax of C++17 fold expressions are as follows:
327
Chapter 7 Functional Programming |
|
|
( ... operator parampack ) |
// left fold |
|
( parampack operator ... ) |
// right |
fold |
( initvalue operator ... operator parampack ) |
// left fold with an init |
|
|
value |
|
( parampack operator ... operator initvalue ) |
// right |
fold with an init |
|
value |
|
Listing 7-28 shows an example, a left fold with an init value.
Listing 7-28. An example of a Left Fold
#include <iostream>
template<typename... PACK>
int subtractFold(int minuend, PACK... subtrahends) { return (minuend - ... - subtrahends);
}
int main() {
const int result = subtractFold(1000, 55, 12, 333, 1, 12); std::cout << "The result is: " << result << std::endl; return 0;
}
Note that a right fold cannot be used in this case due to the lack of associativity of operatort. Fold expressions are supported for 32 operators, including logical operators like ==, &&, and ||.
Listing 7-29 shows another example, which tests that a parameter pack contains at least one even number.
Listing 7-29. Checking Whether a Parameter Pack Contains an Even Value
#include <iostream>
template <typename... TYPE>
bool containsEvenValue(const TYPE&... argument) { return ((argument % 2 == 0) || ...);
}
328