- •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 4 Basics of Clean C++
the resource object managed differently, so that a delete is forbidden and will result in undefined behavior (see the section “Don’t Allow Undefined Behavior” in Chapter 5)? Is it perhaps even an operating system resource that has to be handled in a very special manner?
According to the information hiding principle (see Chapter 3), this should have no relevance for the callers, but in fact we’ve imposed the responsibility for the resource to them. And if the callers do not handle the pointer correctly, it can lead to serious bugs, for example, memory leaks, double deletion, undefined behavior, and sometimes security vulnerabilities.
Strategies for Avoiding Regular Pointers
Choose simple object construction on the stack instead of on the heap
The simplest way to create a new object is simply by creating it on the stack, like so:
#include "Customer.h" // ...
Customer customer;
In this example, an instance of the Customer class (defined in the Customer.h header) is created on the stack. The line of code that creates the instance can usually be found somewhere inside a function’s or method’s body. That means that the instance is destroyed automatically if the function or method runs out of scope, which happens when we return from the function or method.
So far, so good. But what shall we do if an object that was created in a function or method must be returned to the caller?
In old-style C++, this challenge was often coped with in such a way that the object was created on the heap (using the new operator) and then returned from the function as a pointer to this allocated resource.
Customer* createDefaultCustomer() { Customer* customer = new Customer();
// Do something more with customer, e.g. configuring it, and at the end...
return customer;
}
107
Chapter 4 Basics of Clean C++
The comprehensible reason for this approach is that, if we are dealing with a large object, an expensive copy construction can be avoided this way. But we have already discussed the drawbacks of this solution in the previous section. For instance, what will the caller do if the returned pointer is nullptr? Furthermore, the caller of the function is forced to be in charge of the resource management (e.g., deleting the returned pointer in the correct manner).
COPY ELISION
Almost all, especially commercial-grade C++ compilers today, support so-called copy elision techniques. These are optimizations to prevent extra copies of objects in certain situations (depending on optimization settings; as of C++17, copy elision is guaranteed when an object is returned directly).
On the one hand, this is great, because we can get more performant software with less to no effort this way. And it makes returning by value or passing by value large and costly objects, apart from some exceptions, much simpler in practice. These exceptions are limitations of copy elision where this optimization won’t be able to kick in, such as having multiple exit points (return statements) in a function returning different named objects.
On the other hand, we have to keep in mind that copy elision—depending on the compiler and its settings—can influence the program’s behavior. If the copying an object is optimized away, any copy constructor that may be present is also not executed. Furthermore, if fewer objects are created, you can’t rely on a specific number of destructors being called. You shouldn’t put critical code inside copyor move-constructors or destructors, as you can’t rely on them being called. (You will learn that you should avoid implementing these so-called special member functions by hand anyway, in Chapter 5, in the section “The Rule of Zero”!)
Common forms of copy elision are return value optimization (RVO) and named return value optimization (NRVO).
108
Chapter 4 Basics of Clean C++
Named Return Value Optimization
NRVO eliminates the copy constructor and destructor of a named stack-based object that is returned. For instance, a function could return an instance of a class by value like in this simple example:
class SomeClass { public:
SomeClass();
SomeClass(const SomeClass&); SomeClass(SomeClass&&); ~SomeClass();
};
SomeClass getInstanceOfSomeClass() { SomeClass object;
return object;
}
RVO happens if a function returns a nameless temporary object, as with this modified form of the getInstanceOfSomeClass() function:
SomeClass getInstanceOfSomeClass() { return SomeClass ();
}
Important: Even when copy elision takes place and the call of a copy-/move-constructor is optimized away, they must be present and accessible, either hand-crafted or compiler- generated; otherwise, the program is considered ill-formed!
Good news: Since C++11, we can simply return large objects as values without being worried about a costly copy construction.
Customer createDefaultCustomer() { Customer customer;
// Do something with customer, and at the end...
return customer;
}
109
Chapter 4 Basics of Clean C++
The reason that we no longer have to worry about resource management in this case are the so-called move semantics, which are supported since C++11. Simply speaking, the concept of move semantics allows resources to be “moved” from one object to another instead of copying them. The term “move” means, in this context, that the internal data of an object is removed from the old source object and placed into a new object. It is a transfer of ownership of the data from one object to another object, and this can be performed extremely fast. (C++11 move semantics are discussed in detail in Chapter 5.)
With C++11, all Standard Library container classes have been extended to support move semantics. This not only has made them very efficient, but also much easier to handle. For instance, you can return a large vector containing strings from a function in a very efficient manner, as shown in the example in Listing 4-28.
Listing 4-28. Since C++11, a Locally Instantiated and Large Object Can Be Easily Returned by Value
#include <vector> #include <string>
using StringVector = std::vector<std::string>;
const StringVector::size_type AMOUNT_OF_STRINGS = 10'000;
StringVector createLargeVectorOfStrings() { StringVector theVector(AMOUNT_OF_STRINGS, "Test");
return theVector; // Guaranteed no copy construction here!
}
The exploitation of move semantics is one very good way to get rid of lots of regular
pointers. But we can do much more...
In a function’s argument list, use (const) references instead of pointers
Instead of writing...
void function(Type* argument);
...you should use C++ references, like this:
void function(Type& argument);
110