- •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++
The Compiler Is Your Colleague
As I have written elsewhere, the advent of the C++11 language standard fundamentally changed the way that modern and clean C++ programs are designed. Styles, patterns, and idioms that programmers are using while writing modern C++ code are totally different than before. Besides the fact that the newer C++ standards offer many useful new features to write C++ code that is well maintainable, understandable, efficient, and testable, something else has still changed: the role of the compiler!
In former times, the compiler was just a tool to translate the source code into executable machine instructions (object code) for a computer; but now it is increasingly becoming a tool to support the developer on different levels. The three guiding principles for working with a C++ compiler are the following:
•\ |
Everything that can be done at compile time should be done at |
|
compile time. |
•\ |
Everything that can be checked at compile time should be checked at |
|
compile time. |
•\ |
Everything the compiler can know about a program should be |
|
determined by the compiler. |
In former chapters and sections, you’ve experienced in some spots how the compiler can support you. For instance, in the section about move semantics, we’ve seen that modern C++ compilers are able to perform manifold sophisticated optimizations (e.g., copy elision) that we don’t have to care about anymore. In the following sections, I show you how the compiler can support developers and make many things much easier.
Automatic Type Deduction
Do you remember the meaning of the C++ keyword auto before C++11? I’m pretty sure that it was probably the least-known and used keyword in the language. Maybe you remember that auto in C++98 or C++03 was a so-called storage class specifier and has been used to define that a local variable has “automatic duration,” that is, the variable is created at the point of definition and destroyed when the block it was part of is exited. Since C++11, all variables have automatic duration per default unless otherwise
specified. Thus, the previous semantics of auto were becoming useless, and the keyword got a completely new meaning.
159
Chapter 5 Advanced Concepts of Modern C++
Nowadays, auto is used for automatic type deduction, sometimes also called type inference. If it is used as a type specifier for a variable, it specifies that the type of the variable that is being declared will be automatically deduced (or inferred) from its initializer, like in the following examples:
auto theAnswerToAllQuestions = 42; auto iter = begin(myMap);
const auto gravitationalAccelerationOnEarth = 9.80665; constexpr auto sum = 10 + 20 + 12;
auto strings = { "The", "big", "brown", "fox", "jumps", "over", "the", "lazy", "dog" };
auto numberOfStrings = strings.size();
ARGUMENT DEPENDENT NAME LOOKUP (ADL)
Argument Dependent (Name) Lookup (ADL), also known as Koenig Lookup (named after the American computer scientist Andrew Koenig), is a compiler technique to look up an unqualified function name (that is, a function name without a prefixed namespace qualifier) depending on the types of the arguments passed to the function at its call site.
Suppose you have a std::map<K, T> (defined in the <map> header) like the following one:
#include <map> #include <string>
std::map<unsigned int, std::string> words;
Due to ADL, it is not necessary to specify the namespace std if you use the begin() or end() function to retrieve an iterator from the container. You can simply write:
auto wordIterator = begin(words);
The compiler does not just look at the local scope, but also the namespaces that contain the argument’s type (in this case, the namespace of map<K, T>, which is std). Thus, in the previous example, the compiler finds a fitting begin() function for maps in the std- namespace.
In some cases, you need to explicitly define the namespace, for example, if you want to use std::begin() and std::end() with a simple C-style array.
160
Chapter 5 Advanced Concepts of Modern C++
On first sight, using auto instead of a concrete type seems to be a convenience feature. Developers are no longer forced to remember a type’s name. They simply write auto, const auto, auto& (for references), or const auto& (for const references), and the compiler does the rest, because it knows the type of the assigned value. Automatic type deduction can of course also be used in conjunction with constexpr (see the section about computations at compile time).
Do not be afraid to use auto (or auto& and const auto&) as much as possible. The code is still statically typed, and the types of the variables are clearly defined. For instance, the type of the variable strings from the previous example
is std::initializer_list<const char*>, the type of numberOfStrings is std::initializer_list<const char*>::size_type.
The only thing that developers should be aware of is that auto will strip const and reference qualifiers, and hence a careless use of it can result in unwanted copies being made. Especially in range-based for loops, this can easily be overlooked:
#include |
<string> |
|
#include |
<vector> |
|
// And somewhere in the code... |
|
|
std::vector<std::string> aLotOfStrings { .......... |
}; |
for (auto str : aLotOfStrings) {
// Attention: A copy of each string will be made!
}
for (const auto& str : aLotOfStrings) { // Copies are avoided.
}
STD::INITIALIZER_LIST<T> [C++11]
In former days (prior C++11), if we wanted to initialize a Standard Library container using literals, we had to do the following:
std::vector<int> integerSequence; integerSequence.push_back(14); integerSequence.push_back(33); integerSequence.push_back(69); // ...and so on...
161
Chapter 5 Advanced Concepts of Modern C++
Since C++11, we can simply do it this way:
std::vector<int> integerSequence { 14, 33, 69, 104, 222, 534 };
The reason for this is that std::vector<T> has an overloaded constructor that accepts a so- called initializer list as a parameter. An initializer list is an object of type std::initializer_ list<T> (defined in the <initializer_list> header).
An instance of type std::initializer_list<T> is automatically constructed when you use a list of comma-separated literals that are surrounded with a pair of curly braces, a so-called braced-init-list. You can equip your own classes with constructors that can accept initializer lists, as shown in this example:
#include <string> #include <vector>
using WordList = std::vector<std::string>;
class LexicalRepository { public:
explicit LexicalRepository(const std::initializer_list<const char*>& words) { wordList.insert(begin(wordList), begin(words), end(words));
}
// ...
private:
WordList wordList;
};
int main() {
LexicalRepository repo { "The", "big", "brown", "fox", "jumps", "over", "the", "lazy", "dog" };
// ...
return 0;
}
Note This initializer list should not be confused with a class of its constructor member initializer list!
162
Chapter 5 Advanced Concepts of Modern C++
Since C++14, the automatic return type deduction for functions is also supported. This is especially helpful when a return type has a difficult-to-remember or unutterable name, which is often the case when dealing with complex non-standard data types as return types.
auto function() {
std::vector<std::map<std::pair<int, double>, int>> returnValue; // ...fill 'returnValue' with data...
return returnValue;
}
We haven’t discussed lambda expressions until now (they will be discussed in detail in Chapter 7), but C++11 and higher lets you store lambda expressions in named variables:
auto square = [](const int x) { return x * x; };
Maybe you’re wondering why, in Chapter 4, I told you that an expressive and good name is important for the readability of the code and should be a major goal for every professional programmer. Now I promote the use of the keyword auto, which makes it more difficult to recognize the type of a variable quickly just by reading the code. Isn’t that a contradiction?
My clear answer is this: no, quite the contrary! Apart from a few exceptions, auto can raise the readability of the code. Look at the two alternatives of a variable assignment in Listing 5-15.
Listing 5-15. Which One of the Following Two Versions Would You Prefer?
// 1st version: without auto std::shared_ptr<controller::CreateMonthlyInvoicesController> createMonthlyInvoicesController =
std::make_shared<controller::CreateMonthlyInvoicesController>();
// 2nd version: with auto:
auto createMonthlyInvoicesController = std::make_shared<controller::CreateMonthlyInvoicesController>();
From my point of view, the version using auto is easier to read. There is no need to repeat the type explicitly, because it is pretty clear from its initializer what type
163