- •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++
Well, but what are the principles that guide us in developing a good error-handling strategy? When is it justified to throw an exception? How do I deal with thrown exceptions? And for what purposes should exceptions never be used? What are the alternatives?
The following sections present some rules, guidelines, and principles that help C++ programmers design and implement a good error-handling strategy.
Prevention Is Better Than Aftercare
A fundamentally good basic strategy for dealing with errors and exceptions is to generally avoid them. The reason for this is obvious: everything that cannot happen does not have to be treated.
Maybe you will say now, well, this is a truism. Of course it is much better to avoid errors or exceptions, but sometimes it is not possible to prevent them. You’re right, it sounds banal at first glance. And yes, especially when using third-party libraries,
accessing databases, or accessing an external system, unforeseeable things can happen. But for your own code, meaning the parts of the system that you can design as you want, you can take appropriate measures to avoid exceptions as far as possible.
David Abrahams, an American programmer, former ISO C++ standardization committee member, and a founding member of Boost C++ Libraries, created an understanding of what is called exception safety and presented it in a paper [Abrahams98] in 1998. The set of contractual guidelines formulated in this paper, which are also known as the “Abrahams Guarantees,” had a significant influence on the design of the C++ Standard Library and how this library deals with exceptions. But these guidelines are not only relevant to low-level library implementers. They can also be considered by software developers who are writing the application code on higher abstraction levels.
Exception safety is part of the interface design. An interface (API) does not only consist of function signatures, that is, a function’s parameters and return types. The exceptions that might be thrown if a function is invoked are also part of its interface. Furthermore, there are three more aspects that must be considered:
•\ Precondition: A condition that must always be true before a function or a class’s method is invoked. If a precondition is violated, no guarantee can be given that the function call leads to the expected result. The function call may succeed, may fail, can cause unwanted side effects, or show undefined behavior.
197
Chapter 5 Advanced Concepts of Modern C++
•\ Invariant: A condition that must always be true during the execution of a function or method. In other words, it is a condition that is true at the beginning and at the end of a function’s execution. A special form of an invariant in object-orientation is a class invariant. If such an invariant is violated, the object (instance) of the class is left behind in an incorrect and inconsistent state after a method call.
•\ Postcondition: A condition that must always be true immediately after the execution of a function or method. If a postcondition
is violated, an error must have occurred during execution of the function or method.
The idea behind exception safety is that functions, or a class and its methods, give their clients a kind of promise, or a guarantee, about invariants, postconditions, and about exceptions that might be thrown or not thrown. There are four levels of exception safety. In the following subsections, I discuss them shortly in increasing order of safety.
No Exception Safety
With this lowest level of exception safety—literally, no exception safety—absolutely nothing is guaranteed. Any occurring exception can have disastrous consequences. For instance, invariants and postconditions of the called function or method are violated, and a portion of your code, for example, an object, is possibly left behind in a corrupted state.
I think that there is no doubt that the code written by you should never ever offer this inadequate level of exception safety! Just pretend that there is no such thing as “no exception safety.” That’s all; there’s nothing more to say about that.
Basic Exception Safety
The basic exception safety guarantee is the guarantee that any piece of code should offer at least. It is also the exception safety level that can be achieved with relatively little implementation effort. This level guarantees the following:
•\ If an exception is thrown during a function or method call, it is ensured that no resources are leaked! This guarantee includes memory resources as well as other resources. This can be achieved by applying RAII pattern (see the section about RAII and smart pointers).
198
|
Chapter 5 Advanced Concepts of Modern C++ |
•\ |
If an exception is thrown during a function or method call, all |
|
invariants are preserved. |
•\ |
If an exception is thrown during a function or method call, there will |
|
be no corruption of data or memory afterward, and all objects are in |
|
a healthy and consistent state. However, it is not guaranteed that the |
|
data content is the same as before the function or method has been |
|
called. |
Tip The strict rule is this: Design your code, especially your classes, such that they guarantee at least the basic exception safety. This should always be the default exception-safety level!
It is important to know that the C++ Standard Library expects all user types to give at least the basic exception guarantee.
Strong Exception Safety
The strong exception safety guarantees everything that is also guaranteed by the basic exception safety level, but also ensures that in case of an exception, the data is recovered exactly as before the function or method was called. In other words, with this exception- safety level, we get commit or rollback semantics like in transaction handling on databases.
It is easy to comprehend that this exception-safety level leads to a higher implementation effort and can be costly at runtime. An example of this additional effort is the so-called copy-and-swap idiom that must be used to ensure strong exception safety for copy assignment.
Equipping your whole code with strong exception safety without any good reasons would violate the KISS and YAGNI principles (see Chapter 3). Hence, the guideline regarding this is in the following tip.
Tip Issue the strong exception safety guarantee for your code only if it is absolutely required or if the implementation efforts are small compared to the benefits you get (see the Copy-and-Swap idiom discussed in Chapter 9).
199
Chapter 5 Advanced Concepts of Modern C++
Of course, if there are certain quality requirements regarding data integrity and data correctness that have to be satisfied, you have to provide the rollback mechanism that is guaranteed through strong exception safety.
The No-Throw Guarantee
This is the highest exception-safety level, also known as failure transparency. Simply speaking, this level means that as a caller of a function or method, you don’t have to worry about exceptions. The function or method call will succeed. Always! It will never throw an exception, because everything is properly handled internally. There will never be violated invariants and postconditions.
This is the all-round carefree package of exception safety, but it is sometimes very difficult or even impossible to achieve, especially in C++. For instance, if you use any kind of dynamic memory allocation inside a function, like operator new, either directly or indirectly (e.g., via std::make_shared<T>), you have absolutely no chance to end up with a successfully processed function after an exception was encountered.
Here are the cases where the no-throw guarantee is either absolutely mandatory or at least explicitly advised:
•\ Destructors of classes must guarantee to be no-throw under all circumstances! The reason is that, among other situations,
destructors are also called while stack unwinding after an exception has been encountered. It would be fatal if another exception would occur during stack unwinding, because the program would terminate immediately.
As a consequence, any operation inside a destructor that deals with allocated resources and tries to close them, like opened files or allocated memory on the heap, must not throw.
•\ Move operations (move constructors and move assignment operators; see the earlier section about move semantics) should guarantee to be no-throw. If a move operation throws an exception, the probability is enormously high that the move has not taken place. Hence, it should be avoided at all costs that implementations of move operations allocate resources via resource allocation techniques that can throw exceptions. Furthermore, it is important to give the no-throw guarantee for types that are intended to be used with the C++ Standard Library
200
Chapter 5 Advanced Concepts of Modern C++
containers. If the move constructor for an element type in a container doesn’t give a no-throw guarantee (i.e., the move constructor is not declared with the noexcept specifier), then the container will prefer using the copy operations rather than the move operations.
•\ Default constructors should be preferably no-throw. Basically, throwing an exception in a constructor is not desirable, but it is the best way to deal with constructor failures. A “half-constructed object” does highly likely violate invariants. And an object in a corrupt state that violates its class invariants is useless and dangerous. Therefore, there is nothing speaking against throwing an exception in a default constructor when it is unavoidable. However, it is a good design strategy to largely avoid it. Default constructors should be simple. If a default constructor can throw, it is probably doing too many complex things. Hence, when designing a class, you should try to avoid exceptions in the default constructor.
•\ A swap function must guarantee to be no-throw under all circumstances! An expertly implemented swap() function should not allocate any resources (e.g., memory) using memory allocation techniques that potentially can throw exceptions. It would be fatal if swap() can throw, because it can end up with an inconsistent state. The best way to write an exception-safe operator=() is using a non- throwing swap() function for its implementation (see the Copy-and- Swap idiom in Chapter 9).
NOEXCEPT SPECIFIER AND OPERATOR [C++11]
Prior to C++11, the throw keyword could be in a function’s declaration. It was used to list all exception types in a comma-separated list that a function might directly or indirectly throw, known as the dynamic exception specification. The usage of throw(exceptionType, exceptionType, ...) is deprecated since C++11 and has been finally removed from the standard in C++17! What was still available, but also marked as deprecated since C++11, was the throw() specifier without an exception type list. This has now also been removed from the standard with C++20. Its semantics are now the same as the noexcept(true) specifier.
201