- •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++
Avoid Output Parameters
An output parameter, sometimes also called a result parameter, is a function parameter that is used for the function’s return value.
One of the frequently mentioned benefits of using output parameters is that functions that use them can pass back more than one value at a time. Here is a typical example:
bool ScriptInterpreter::executeCommand(const std::string& name, const std::vector<std::string>& arguments, Result& result);
This member function of the ScriptInterpreter class returns not only a bool. The third parameter is a non-const reference to an object of type Result, which represents the real result of the function. The Boolean return value determines whether the execution of the command was successful by the interpreter. A typical call of this member function might look like this:
ScriptInterpreter interpreter; // Many other preparations...
Result result;
if (interpreter.executeCommand(commandName, argumentList, result)) {
//Continue normally...
}else {
//Handle failed execution of command...
Tip Avoid output parameters at all costs.
Output parameters are unintuitive and can lead to confusion. The caller can sometimes not determine whether a passed object is treated as an output parameter and will possibly be mutated by the function.
Furthermore, output parameters complicate the easy composition of expressions. If functions have only one return value, they can be interconnected quite easily to chained function calls. In contrast, if functions have multiple output parameters, developers
are forced to prepare and handle all the variables that will hold the resultant values. Therefore, the code that calls these functions can turn into a mess quickly.
103
Chapter 4 Basics of Clean C++
Especially if immutability should be fostered and side effects must be reduced, output parameters are an absolutely terrible idea. Unsurprisingly, it is still impossible to pass an immutable object (see Chapter 9) as an output parameter.
If a method should return something to its callers, let the method return it as the method’s return value. If the method must return multiple values, redesign it to return a single instance of an object that holds the values.
Alternatively, the class template std::pair can be used. The first member variable is assigned the Boolean value indicating success or fail, and the second member variable is assigned the real return value. However, both std::pair and its “big brother” std::tuple (available since C++11) are, from my point of view, always a design smell. A std::pair<bool, Result> is not really a speaking name. If you decide to use something like that, and I would not recommend it anyway, you should at least introduce a meaningful alias name with the help of the using declaration.
Another possibility is to use a std::optional, a class template that is defined in the <optional> header and available since C++17. As its name suggest, objects of this class template can manage an optional contained value, i.e., a value that may or may not be present.
In addition to the aforementioned solutions, there is one more. You can use the so-called special case object pattern to return an object representing an invalid result. Since this is a object-oriented design pattern, I introduce it in Chapter 9.
Here is my final advice about how to deal with return parameters: As mentioned, avoid output parameters. If you want to return multiple values from a function or method, introduce a small class with well-named member variables to bundle all the data that you want to return to the call site. You may find after a short while that this class should have existed anyway and you can put some logic in it.
Don’t Pass or Return 0 (NULL, nullptr)
THE BILLION DOLLAR MISTAKE
Sir Charles Antony Richard Hoare, commonly known as Tony Hoare or C. A. R. Hoare, is a famous British computer scientist. He is primarily known for the Quick Sort algorithm. In 1965, Tony Hoare worked with the Swiss computer scientist Niklaus E. Wirth on the further development of the programming language ALGOL. He introduced null references in the programming language ALGOL W, which was the predecessor of PASCAL.
104
Chapter 4 Basics of Clean C++
More than 40 years later, Tony Hoare regrets this decision. In a talk at the QCon 2009 Conference in London, he said that the introduction of null references had probably been a billion dollar mistake. He argued that null references have caused so many problems in the past decades that the cost could be approximated at $USD 1 billion.
In C++, pointers can point to NULL or 0. Concretely, this means that the pointer points to the memory address 0. NULL is just a macro definition:
#define NULL |
0 |
Since C++11, the language provides the new keyword called nullptr, which is of type std::nullptr_t.
Sometimes I see functions like this one:
Customer* findCustomerByName(const std::string& name) const {
//Code that searches the customer by name...
//...and if the customer could not be found: return nullptr; // ...or NULL;
}
Receiving NULL or nullptr as a return value from a function can be confusing. (Starting from here, I will only use nullptr in the following text, because the C-style macro NULL has no place in modern C++ anymore.) What should the caller do with it? What does it mean? In the previous example, it might be that a customer with the given name does not exist. But it can also mean that there has been a critical error. A nullptr can mean failure, can mean success, and can mean almost anything.
Note If it is inevitable to return a regular pointer as the result from a function or method, do not return nullptr!
In other words, if you’re forced to return a regular pointer as the result from a function (we will see later that there may be better alternatives), ensure that the pointer you’re returning always points to a valid address. Here are my reasons why I think this is important.
The main rationale why you should not return nullptr from a function is that you shift the responsibility to decide what to do to your callers. They have to check it. They
105
Chapter 4 Basics of Clean C++
have to deal with it. If functions can potentially return nullptr, this leads to many null checks, like this:
Customer* customer = findCustomerByName("Stephan");
if (customer != nullptr) {
OrderedProducts* orderedProducts = customer->getAllOrderedProducts(); if (orderedProducts != nullptr) {
//Do something with orderedProducts...
}else {
//And what should we do here?
}
}else {
//And what should we do here?
Many null checks reduce the readability of the code and increase its complexity. And there is another visible problem that leads us directly to the next point.
If a function can return a valid pointer or nullptr, it introduces an alternative flow path that needs to be continued by the caller. And it should lead to a reasonable and sensible reaction. This is sometimes quite problematic. What would be the correct, intuitive response in our program when our pointer to Customer is not pointing to a valid instance, but nullptr? Should the program abort the running operation with a message? Are there any requirements that a certain type of program continuation is mandatory in such cases? These questions sometimes cannot be answered well. Experience has shown that it is often relatively easy for stakeholders to describe all the so-called happy day cases of their software, which are the positive cases during normal operation. It is much more difficult to describe the desired behavior of the software during the exceptions, errors, and special cases.
The worst consequence may be this: If any null check is forgotten, this can lead to critical runtime errors. Dereferencing a null pointer will lead to a segmentation fault and your application will crash.
In C++ there is still another problem to consider: object ownership.
For the caller of the function, it is unclear what to do with the resource pointed to by the pointer after its usage. Who is its owner? Is it required to delete the object? If yes, how is the resource to be disposed? Must the object be deleted with delete, because it was allocated with the new operator somewhere inside the function? Or is the ownership of
106