- •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++
No Ownership, but Secure Access with std::weak_ptr<T>
Sometimes it is necessary to have a non-owning pointer to a resource that is owned by one or more shared pointers. At first you might say, okay, but what’s the problem? I
simply can obtain the raw pointer from an instance of std::shared_ptr<T> at any time by calling its get() member function. See Listing 5-5.
Listing 5-5. Retrieving the Regular Pointer from an Instance of std::shared_ptr<T>
std::shared_ptr<ResourceType> resource { std::make_shared<ResourceType>() };
// ...
ResourceType* rawPointerToResource { resource.get() };
Watch your step! This could be dangerous. What will happen if the last instance of std::shared_ptr<ResourceType> gets destroyed somewhere in your program and this raw pointer is still in usage somewhere? The raw pointer will point to No-Man’s-
Land and using it can cause serious problems (remember my warning about undefined behavior in the previous chapter). You have absolutely no chance to determine that the raw pointer points to a valid address of a resource, or to an arbitrary location in memory. If you need a pointer to the resource without having ownership, you should use
std::weak_ptr<T> (defined in the <memory> header), which has no influence on the resource’s lifetime. std::weak_ptr<T> merely “observes” the managed resource and can be interrogated that it is valid. See Listing 5-6.
Listing 5-6. Using std::weak_ptr<T> to Deal with Resources That Are Not Owned
01 #include <memory>
02
03 void doSomething(const std::weak_ptr<ResourceType>& weakResource) {
04 if (! weakResource.expired()) {
05 // Now we know that weakResource contains a pointer to a valid object 06 std::shared_ptr<ResourceType> sharedResource = weakResource.lock(); 07 // Use sharedResource...
08 }
09 }
10
139
Chapter 5 Advanced Concepts of Modern C++
11int main() {
12auto sharedResource{ std::make_shared<ResourceType>() };
13std::weak_ptr<ResourceType> weakResource{ sharedResource };
15doSomething(weakResource);
16sharedResource.reset(); // Deletes the managed instance of ResourceType
17doSomething(weakResource);
18
19return 0;
20}
As you can see on Line 4 in Listing 5-6, we can interrogate the weak pointer object if it manages a valid resource. This is done by calling its expired() member function. std::weak_ptr<T> does not provide dereference operators, like *, or ->. If we want to use the resource, we first must call the lock() function (see line 6) to obtain a shared pointer object from it.
You might be asking yourself now what the use cases of this smart pointer type are. Why is it necessary, because you could readily also take a std::shared_ptr<T> everywhere a resource is needed?
First of all, with std::shared_ptr<T> and std::weak_ptr<T>, you are able to distinguish between owners of a resource and users of a resource in a software design. Not every software unit that requires a resource just for a certain and time-limited task wants to become its owner. As we can see in the function doSomething() in the previous example, sometimes it is sufficient just to “promote” a weak pointer to a strong pointer for a limited amount of time.
A good example would be an object cache that for the purpose of performance efficiency keeps recently accessed objects in memory for a certain amount of time. The objects in the cache are held with std::shared_ptr<T> instances, together with a last-used timestamp. Periodically, a kind of garbage collector process is running that scans the cache and decides to destroy those objects that have not been used for a defined time span.
At those places where the cached objects are used, instances of std::weak_ptr<T> are used to hold non-owning pointers to these objects. If the expired() member function of those std::weak_ptr<T> instances returns true, the garbage collector process has cleared the objects from the cache. In the other case, the std::weak_ptr<T>::lock() function can be used to retrieve a std::shared_ptr<T> from it. Now the object can be
140
Chapter 5 Advanced Concepts of Modern C++
safely used, even if the garbage collector process gets active. The process could evaluate the usage counter of the std::shared_ptr<T> and ascertain that the object has currently at least one user outside the cache. As a consequence, the object’s lifetime would be extended. Or the process could delete the object from the cache, which does not interfere with its users.
Another example is to deal with circular dependencies. For instance, if you have a class A that needs a pointer to another class B and vice versa, you will end up with a circular dependency. If you use std::shared_ptr<T> to point to the respective class, as shown in Listing 5-7, you can end up with a memory leak. The reason for this is that
the usage counter in the respective shared pointer instance will never count down to 0. Thus, the objects will never be deleted.
Listing 5-7. The Problem with Circular Dependencies Caused Through Careless Use of std::shared_ptr<T>
#include <memory>
class B; // Forward declaration
class A { public:
void setB(std::shared_ptr<B>& pointerToB) { myPointerToB = pointerToB;
}
private:
std::shared_ptr<B> myPointerToB;
};
class B { public:
void setA(std::shared_ptr<A>& pointerToA) { myPointerToA = pointerToA;
}
private:
std::shared_ptr<A> myPointerToA;
};
141
Chapter 5 Advanced Concepts of Modern C++
int main() {
{// Curly braces build a scope
auto pointerToA = std::make_shared<A>(); auto pointerToB = std::make_shared<B>(); pointerToA->setB(pointerToB); pointerToB->setA(pointerToA);
}
//At this point, one instance each of A and B is "lost in space" (memory leak!)
return 0;
}
If the std::shared_ptr<T> member variables in the classes are replaced with non-owning weak pointers (std::weak_ptr<T>) to the respective other class, the issue with the memory leak is solved. See Listing 5-8.
Listing 5-8. Circular Dependencies Implemented the Right Way with std::weak_ptr<T>
class B; // Forward declaration
class A { public:
void setB(std::shared_ptr<B>& pointerToB) { myPointerToB = pointerToB;
}
private:
std::weak_ptr<B> myPointerToB;
};
class B { public:
void setA(std::shared_ptr<A>& pointerToA) { myPointerToA = pointerToA;
}
142