- •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
int main() {
const bool result1 = containsEvenValue(10, 7, 11, 9, 33, 14); const bool result2 = containsEvenValue(17, 7, 11, 9, 33, 29);
std::cout << std::boolalpha;
std::cout << "result1 is " << result1 << "\n"; std::cout << "result2 is " << result2 << std::endl; return 0;
}
The output of this program is as follows:
result1 is true result2 is false
Pipelining with Range Adaptors (C++20)
People who like to work with UNIX or Linux operating systems emphasize, among other things, a particularly convenient and efficient way to perform tasks on these operating systems: using shell programming. Basically, a UNIX/Linux shell is a command-line-based human-machine interface. There are a number of text-based shells available for UNIX/ Linux OS, such as Bash (an acronym for Bourne Again Shell), the Korn Shell, and the Z Shell.
One of the reasons that you can perform complex tasks very elegantly and efficiently with the help of these shells is the possibility of pipelining. Some people say the concept of pipelining, introduced in 1972, has been one of the most important UNIX innovations in history, perhaps apart from regular expressions. Basically, a pipeline is a message-passing pattern and describes a chain of processing elements. In a shell, a pipeline is a set of processes, usually small programs, chained together by their standard streams, so that the output text of each process (stdout) is passed directly as input (stdin) to the next one.
As an example, suppose we have a text file named customers.txt, in which hundreds of customer names are listed line-by-line, preceded by a date, as follows (an excerpt):
2020-11-05,Stephan Roth
2020-11-22,John Doe
2020-10-15,Mark Powers [...]
329
Chapter 7 Functional Programming
On the Bash command line, we now execute the following command sequence:
$ cat customers.txt | sort -r > customers2.txt
What will happen here? Well, first the cat command (short for “concatenate”) lists the contents of the text file customers.txt on stdout. This output stream is redirected with the help of the pipe operator (a vertical bar: |) and is used as the input stream for the program sort. The sort command is used to sort lines in a text file. In our case, we have specified by the command-line option -r that the sort order should be reversed.
Instead of descending sorting, ascending sorting is used. The output stream of the sort command is redirected again, because we don’t want to see its output on the screen. Instead, it should be written to a new text file. The greater than symbol, >, tells the shell to redirect the sort’s output to the customers2.txt file.
In Chapter 5 I briefly introduced the new C++20 Ranges library. However, I neglected a few of the new features there: range adaptors and chaining using the pipe operator! Just as you can chain commands together on a UNIX shell using pipes, this is also possible with C++20 Range adaptors.
You may remember the following small code example (excerpt) from Listing 5-26? It’s shown in again in Listing 7-30.
Listing 7-30. The Code Snippet from Listing 5-26
#include <iostream>
#include <ranges> #include <vector>
std::vector<int> integers = { 2, 5, 8, 22, 45, 67, 99 };
auto view = std::views::reverse(integers); // does not change 'integers'
Recall that a view is lazy evaluated, i.e., whatever transformation it applies, the view performs it at the moment someone requests an element. And as a range adaptor, it does not modify the underlying range, in our case the vector named integers.
Due to their property as adaptors, views can be easily chained. We now extend this example by adding more views and some lambda expressions; see Listing 7-31.
330
Chapter 7 Functional Programming
Listing 7-31. Chaining Range Adaptors
#include <algorithm> // required for std::ranges::for_each #include <iostream>
#include <ranges> #include <vector>
int main() {
std::vector<int> integers = { 2, 5, 8, 22, 45, 67, 99 }; auto isOdd = [] (const int value) { return value % 2 != 0; }; auto square = [] (const int value) { return value * value; };
auto printOnStdOut = [] (const int value) { std::cout << value << '\n';
};
auto view = std::views::transform(std::views::reverse(std::views::filter( integers, isOdd)),
square);
std::ranges::for_each(view, printOnStdOut); return 0;
}
The output of the program is as follows:
9801
4489
2025
25
Well, from a clean code developer’s perspective, this code sample still has one unsightly flaw: the nested Range adaptors for creating the view. This is just one line of code, but it is not easy to comprehend on first sight what’s happening here. Just think about that in a real software application; nested Range adaptors could become much more complex than in this relatively simple example.
331
Chapter 7 Functional Programming
This is where the new C++20 pipe operator comes into play. It is “syntactic sugar”1 and can be used for easier function chaining. The line of code in which the view view is created can also be written as follows:
auto view = integers | std::views::filter(isOdd) | std::views::reverse | std::views::transform(square);
That’s pretty convenient, isn’t it? This looks very similar to building pipelines in a UNIX shell, as we saw it at the beginning of this section. You can simply read the
expression from left to right and easily understand how the view is composed. Thanks to the power of views, the C++20 Ranges Library enables developers to write code in an even more functional programming-style.
Before we come to the end of this chapter on functional programming, I previously announced that I would revisit and improve the code example in Listing 7-15 once again. With all the new functional programming concepts we have now learned, we can refactor this example and make it much more compact and elegant; see Listing 7-32.
Listing 7-32. The Refactored Code Example
#include <concepts> #include <iostream>
#include <ranges>
template <typename T>
concept Streamable = requires (std::ostream& os, const T& value) { { os << value };
};
int main() {
auto toSquare = [] (const auto value) { return value * value; }; auto isAnEvenNumber = [] <std::integral T> (const T value) {
return (value % 2) == 0;
};
auto print = [] <Streamable T> (const T& printable) { std::cout << printable << '\n';
};
1The term “syntactic sugar” describes syntax within a programming language that is designed to make parts of the code easier to read or to express.
332