- •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 9 Design Patterns and Idioms
The Copy-and-Swap Idiom
In the section “Prevention Is Better Than Aftercare” in Chapter 5, we learned the four levels of exception-safety guarantee: no exception-safety, basic exception-safety, strong exception-safety, and the no-throw guarantee. What member functions of a class should always guarantee is the basic exception-safety, because this exception-safety level is usually easy to implement.
In the section “The Rule of Zero” in Chapter 5, we learned that we should design classes in a way that the compiler-generated special member functions (copy constructor, copy assignment operator, etc.) automatically do the right things. Or in other words, when we are forced to provide a non-trivial destructor, we are dealing with an exceptional case that requires special treatment during destruction of the object. As a consequence, it follows that the special member functions generated by the compiler are not sufficient to deal with this situation, and we have to implement them by ourselves.
However, it is inevitable that the Rule of Zero will occasionally not be fulfilled, that is, a developer has to implement the special member functions by herself. In this case, it may be a challenging task to create an exception-safe implementation of an overloaded assignment operator. In such a case, the Copy-and-Swap idiom is an elegant way to solve this problem.
Hence, the intent of this idiom is as follows:
“Implement the copy assignment operator with strong exception safety.”
The simplest way to explain the problem and its solution is with a small example. Consider the class shown in Listing 9-47.
Listing 9-47. A Class That Manages a Resource That Is Allocated on the Heap
#include <cstddef>
class Clazz final { public:
explicit Clazz(const std::size_t size) : resourceToManage { new char[size] }, size { size } { }
~Clazz() {
delete [] resourceToManage;
}
443
Chapter 9 Design Patterns and Idioms
private:
char* resourceToManage; std::size_t size;
};
This class is, of course, only for demonstration purposes and should not be part of a real program.
Let’s assume that we want to do the following with the class Clazz:
int main() {
Clazz instance1 { 1000 }; Clazz instance2 { instance1 }; return 0;
}
We know from Chapter 5 that the compiler-generated version of a copy constructor does the wrong thing here: it only creates a flat copy of the character pointer resourceToManage!
Hence, we have to provide our own copy constructor, like so:
#include <algorithm>
class Clazz final { public:
// ...
Clazz(const Clazz& other) : Clazz { other.size } { std::copy(other.resourceToManage, other.resourceToManage + other.size, resourceToManage);
}
// ...
};
So far, so good. Now the copy construction will work fine. But now we’ll also need a copy assignment operator. If you are not familiar with the copy-and-swap idiom, an implementation of an assignment operator might look like this:
#include <algorithm>
class Clazz final {
444
Chapter 9 Design Patterns and Idioms
public: // ...
Clazz& operator=(const Clazz& other) { if (&other == this) {
return *this;
}
delete [] resourceToManage; resourceToManage = new char[other.size];
std::copy(other.resourceToManage, other.resourceToManage + other.size, resourceToManage);
size = other.size; return *this;
}
// ...
};
Basically, this assignment operator will work, but it has several drawbacks. For instance, the constructor and destructor code is duplicated in it, which is a violation of the DRY principle (see Chapter 3). Furthermore, there is a self-assignment check at the beginning. But the biggest disadvantage is that we cannot guarantee exception-safety. For example, if the new statement causes an exception, the object can be left behind in a weird state that violates elementary class invariants.
Now the copy-and-swap idiom comes into play, also known as “Create-Temporary- and-Swap”!
For a better understanding, I present the whole class Clazz in Listing 9-48.
Listing 9-48. A Much Better Implementation of an Assignment Operator Using the Copy-and-Swap Idiom
#include <algorithm> #include <cstddef>
class Clazz final { public:
explicit Clazz(const std::size_t size) : resourceToManage { new char[size] },
size { size } { }
445
Chapter 9 Design Patterns and Idioms
~Clazz() {
delete [] resourceToManage;
}
Clazz(const Clazz& other) : Clazz { other.size } { std::copy(other.resourceToManage, other.resourceToManage + other.size,
resourceToManage);
}
Clazz& operator=(Clazz other) { swap(other);
return *this;
}
private:
void swap(Clazz& other) noexcept { using std::swap;
swap(resourceToManage, other.resourceToManage); swap(size, other.size);
}
char* resourceToManage{ nullptr }; std::size_t size{ 0 };
};
What is the trick here? Let’s look at the completely different assignment operator. This no longer has a const reference (const Clazz& other) as a parameter, but an ordinary value parameter (Clazz other). This means that when this assignment operator is called, the copy constructor of Clazz is called. The copy constructor, in turn, calls the default constructor that allocates memory for the resource. And that is exactly what we want: we need a temporary copy of other!
Now we come to the heart of the idiom: the call of the private member function Clazz::swap(). Within this function, the contents of the temporary instance other, that is, its member variables, are exchanged (“swapped”) with the contents of the same member variables of our own class context (this). This is done by using the non-throwing std::swap() function (defined in the <utility> header). After the swap operations, the temporary object called other now owns the resources that were previously owned by the this object, and vice versa.
446