![](/user_photo/_userpic.png)
- •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++
•\ Hard to misuse. Use appropriate parameter and return types and avoid long parameter lists (see the section entitled “Arguments and Return Values” in Chapter 4). If values have semantics, strongly typed parameters instead of primitive data types (int, double, ...) should be used for them, as described in the section “Type-Rich Programming” in this chapter. Don’t use a string if a better type exists.
•\ Don’t forget that exceptions are also part of an interface. Throw exceptions only to indicate true exceptional conditions, i.e. don’t force users of your interface to use exceptions for normal control flow. This aspect has been discussed in detail in the previous section entitled “Proper Exception and Error Handling.”
•\ Provide a suite of well-crafted unit tests for your API. As discussed in Chapter 2, a suite of good tests is not only a sign of the quality awareness of the developers, but they are also good examples for users that can show how to use the API.
In addition to these general good practices for interface design, modern C++ offers further possibilities to specify interfaces, which I briefly discuss in the following and last sections of this chapter:
•\ |
Attributes (since C++11) |
•\ |
Concepts (new since C++20) |
Attributes
C++ Attributes were introduced with C++11 and regularly extended with the following language standards. Maybe you know a very similar concept in programming language Java, which is called annotations. Some attributes are part of the C++ language standard, others are compiler-specific.
In simple terms, an attribute is an expression surrounded with double square brackets to give instructions to the compiler, like this:
[[attr]]
Multiple attributes can be specified as a comma-separated list:
[[attr1, attr2, attr3]]
210
Chapter 5 Advanced Concepts of Modern C++
Specific kinds of attributes can also have an argument:
[[attr(argument)]]
With attributes, software developers can specify additional information or instructions for the compiler, e.g., to enforce constraints (conditions), optimize certain sections of code, or do some specific code generations. Basically, attributes can be applied to almost every C++ programming language construct, e.g., types, variables, functions/methods, names, code blocks, and so on. However, certain attributes only make sense for very specific parts of the code. And they can also be very useful to design interfaces.
In the following sections, I introduce some of the attributes that are defined in the C++ standard and that can be used in interface design.
noreturn (since C++11)
The attribute [[noreturn]] can be used to mark a function from which the program flow does not return.
[[noreturn]] void function() { while (true) {
// ... do something ...
}
}
Perhaps you might wonder what this is good for? Well, if you implement a function that should intentionally not return (e.g., an endless loop to process events), but does so anyway due to a programming error, you’ll get a compiler warning:
warning: 'noreturn' function does return
deprecated (since C++14)
Sometimes it is necessary to take back parts of an already published interface. As mentioned, ideally this should not happen, because users of an interface have made themselves dependent on it. At the same time, it is sometimes unavoidable in reality.
A good idea is not to remove the published part of the interface immediately, but to prepare the users that this could happen in the future. In other words, it is advisable to
211
Chapter 5 Advanced Concepts of Modern C++
give your API users a grace period. Therefore, you can mark such entities as deprecated, meaning their use is allowed, but discouraged for some reason.
class SomeType { public:
[[deprecated]] void doSomething() { // ...
}
};
It is also possible to specify a rationale as a string-literal to explain why the use is discouraged:
class SomeType { public:
[[deprecated("This function will be removed in future versions, " "use SomeType::doSomethingNew() instead!")]]
void doSomething() { // ...
}
void doSomethingNew() { // ...
}
};
nodiscard (since C++17)
With the help of the [[nodiscard]] attribute interface, designers can indicate that a return value of a function shouldn’t be ignored. If the return value is ignored at the call site, the compiler generates a warning. Since C++20, you can also specify a rationale as a string-literal to explain to users why ignoring the return value is discouraged. See Listing 5-31.
212
Chapter 5 Advanced Concepts of Modern C++
Listing 5-31. The [[nodiscard]] Attribute Reminds Users to Accept the Return Value
#include <memory>
class SomeType { };
using SomeTypePtr = std::shared_ptr<SomeType>;
class ObjectFactory { public:
[[nodiscard]] SomeTypePtr createInstance() const { return std::make_shared<SomeType>();
}
};
int main() { ObjectFactory factory;
auto instance = factory.createInstance(); // OK! factory.createInstance(); // Compiler warning! return 0;
}
maybe_unused (since C++17)
This attribute can be used to mark entities that might not be used. Thus, a compiler warning can be suppressed, which is generated when variables, parameters of functions or methods, data types, and other entities are declared, but not used.
For instance, depending on the configured warning level of your compiler, the following piece of code will produce a warning like "'param2': unreferenced formal parameter":
int function(const int param1, const int param2) { return param1 + param1;
}
int main() { function(10, 20); return 0;
}
213
Chapter 5 Advanced Concepts of Modern C++
With the attribute [[maybe_unused]], this parameter can be marked so that the compiler warning is suppressed.
int function(const int param1, [[maybe_unused]] const int param2) { return param1 + param1;
}
You might be wondering how you’d use this attribute. You might ask yourself, who intentionally introduces a function parameter that is not used inside the function?
Think about conditional compiling with C++ templates. Listing 5-32 shows a simple example.
Listing 5-32. If Only Param1 Is Needed, You’ll Get No Warning
#include <type_traits>
template<typename T, typename U>
void function(T param1, [[maybe_unused]] U param2) { if constexpr (std::is_floating_point<U>::value) {
//...code that uses 'param1' and 'param2'...
}else {
//...code that uses 'param1' only...
}
}
int main() { function(10, 20.0); function(10, 20); return 0;
}
In the main() function, we see two instantiations of the template function(): the first with one int and one double, and the second one with two ints. In the implementation of function(), we can see a constexpr if, or in other words, a compile-time-if, a new language feature that was introduced with C++17. This feature allows template designers to discard branches of an if statement at compile-time based on a constant expression condition. In our case, it is a type trait (defined in the <type_traits> header) that inspects the type U of param2 and returns true if it is a floating-point type. So, instantiating the template with two ints would result in an unused param2.
214