- •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++
}catch (...) {
//Perform error handling here...
}
As it can be easily seen, no new or delete is required. If resource runs out of scope, which can happen at various points in this method, the wrapped instance of type ResourceType is deleted automatically through the destructor of ScopedResource.
But there is usually no need to reinvent the wheel and to implement such a wrapper, which is also called a smart pointer.
Smart Pointers
Since C++11, the Standard Library offers different, efficient smart-pointer- implementations for easy use. These pointers have been developed over a long period within the well-known Boost library project before they were introduced into the C++ standard, and can be regarded as foolproof as possible. Smart pointers reduce the likelihood of memory leaks. Furthermore, their reference counter mechanism is designed to be thread-safe.
This section provides a brief overview.
Unique Ownership with std::unique_ptr<T>
The class template std::unique_ptr<T> (defined in the <memory> header) manages a pointer to an object of type T. As the name suggests, this smart pointer provides unique ownership, that is, an object can be owned by only one instance of std::unique_ptr<T> at a time, which is the main difference of the std::shared_ptr<T>, which is explained next. This also means that copy construction and copy assignment are not allowed.
Its use is pretty simple:
#include <memory>
class ResourceType { //...
};
//...
std::unique_ptr<ResourceType> resource1 { std::make_unique<ResourceType>() };
135
Chapter 5 Advanced Concepts of Modern C++
// ... or shorter with type deduction ...
auto resource2 { std::make_unique<ResourceType>() };
After this construction, resource can be used very much like a regular pointer to an instance of ResourceType. (std::make_unique<T> is explained in the section entitled “Avoid new and delete”). For example, you can use the * and -> operators for dereferencing:
resource->foo();
Of course, if resource runs out of scope, the contained instance of type ResourceType is freed safely. But the best part is that resource can be easily put into containers, for example, in a std::vector:
#include "ResourceType.h"
#include <memory> #include <vector>
using ResourceTypePtr = std::unique_ptr<ResourceType>; using ResourceVector = std::vector<ResourceTypePtr>;
//...
ResourceTypePtr resource { std::make_unique<ResourceType>() }; ResourceVector aCollectionOfResources; aCollectionOfResources.push_back(std::move(resource));
// IMPORTANT: At this point, the instance of 'resource' is empty!
Note that we ensure that std::vector::push_back() calls the move constructor and the move assignment operator of std::unique_ptr<T> (see the section about move semantics in the next chapter). As a consequence, resource does not manage an object anymore and is denoted as empty.
As mentioned, copy construction of std::unique_ptr<T> is not allowed. However, the exclusive ownership of the managed resource can be transferred to another instance of std::unique_ptr<T>, using move semantics (we will discuss move semantics in detail in a later section) in the following way:
std::unique_ptr<ResourceType> pointer1 { std::make_unique<ResourceType>() }; std::unique_ptr<ResourceType> pointer2; // pointer2 owns nothing yet
pointer2 = std::move(pointer1); |
// Now pointer1 is empty, pointer2 |
|
is the new owner |
136
Chapter 5 Advanced Concepts of Modern C++
Shared Ownership with std::shared_ptr<T>
Instances of class template std::shared_ptr<T> (defined in the <memory> header) can take ownership of a resource of type T and can share this ownership with other instances of std::shared_ptr<T>. In other words, the ownership for a single instance of type T, and thus the responsibility for its deletion, can be taken over by many shared owners.
std::shared_ptr<T> provides something like simple limited garbage collector functionality. The smart pointer’s implementation has a reference counter that monitors how many pointer instances owning the shared object still exist. It releases the managed resource if the last instance of the pointer is destroyed.
Figure 5-1 depicts a class diagram, as well as an object diagram. The lower area of the figure, where the object diagram can be seen, depicts a situation (snapshot) in a running system where three anonymous instances of the class Client share the same resource (:Resource) using three std::shared_ptr instances. The _M_use_count attribute represents the reference counter of std::shared_ptr.
137
Chapter 5 Advanced Concepts of Modern C++
Figure 5-1. Three clients are sharing one resource through smart pointers
In contrast to the previously discussed std::unique_ptr<T>, std::shared_ptr<T> is of course copy-constructible as expected. But you can ensure that the managed resource is moved by using std::move<T>:
std::shared_ptr<ResourceType> pointer1 { std::make_shared<ResourceType>() }; std::shared_ptr<ResourceType> pointer2;
pointer2 = std::move(pointer1); // The reference count does not get modified, pointer1 is empty
In this case, the reference counter is not modified, but you must be careful when using the variable pointer1 after the move, because it is empty, that is, it holds a nullptr. Move semantics and the utility function std::move<T> are discussed in a later section.
138