- •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++ |
|
•\ @post for the postconditions of an entity. |
|
•\ @throws to document the exception object an entity can throw |
|
and the reasons for the exception. |
•\ |
I would not use the @example tag to provide a comment block |
|
containing a source code example about how to use an API. As |
|
mentioned, such comments add a lot of noise to the code. Instead, I |
|
would offer a suite of well-crafted unit tests (see Chapter 2 about unit |
|
tests and Chapter 8 about test-driven development), as these are the |
|
best examples of use—executable examples! In addition, unit tests |
|
are always correct and up to date, as they must be adjusted when the |
|
API changes (otherwise the tests will fail). A comment with a usage |
|
example, on the other hand, can become wrong without anyone |
|
noticing it. |
•\ |
Once a project has been grown to a particular size, it is advisable to |
|
pool certain categories of software units with the help of Doxygen’s |
|
grouping mechanisms (Tags: @defgroup <name>, @addtogroup |
|
<name>, and @ingroup <name>). This is, for example, very useful when |
|
you want to express the fact that certain software units belong to a |
|
cohesive module on a higher level of abstraction (e.g., a component |
|
or subsystem). This mechanism also allows certain categories of |
|
classes to be grouped together, for example all entities, all adapters |
|
(see the section entitled “Adapter Pattern” in Chapter 9), or all object |
|
factories (see the section entitled “Factory Pattern” in Chapter 9). |
|
The CustomerAccount class from the previous code example is, for |
|
instance, in the group of entities (a group that contains all business |
|
objects), but it is also part of the accounting component. |
Functions
Functions (methods, procedures, services, operations) are the heart of any software system. They represent the first organizational unit above the lines of code. Well-written functions foster the readability and maintainability of a program considerably. For this reason, they should be well crafted in a careful manner. In this section, I give several important clues for writing good functions.
89
Chapter 4 Basics of Clean C++
However, before I explain the things that I consider to be important for well-crafted functions, let’s examine a deterrent example again, taken from Apache’s OpenOffice 3.4.1. See Listing 4-20.
Listing 4-20. Another Excerpt from Apache’s OpenOffice 3.4.1 Source Code
1780 |
sal_Bool BasicFrame::QueryFileName(String& rName, FileType nFileType, |
|
sal_Bool bSave ) |
1781 |
{ |
1782 |
NewFileDialog aDlg( this, bSave ? WinBits( WB_SAVEAS ) : |
1783 |
WinBits( WB_OPEN ) ); |
1784 |
aDlg.SetText( String( SttResId( bSave ? IDS_SAVEDLG : IDS_LOADDLG |
|
) ) ); |
1785 |
|
1786 |
if ( nFileType & FT_RESULT_FILE ) |
1787 |
{ |
1788 |
aDlg.SetDefaultExt( String( SttResId( IDS_RESFILE ) ) ); |
1789 |
aDlg.AddFilter( String( SttResId( IDS_RESFILTER ) ), |
1790 |
String( SttResId( IDS_RESFILE ) ) ); |
1791 |
aDlg.AddFilter( String( SttResId( IDS_TXTFILTER ) ), |
1792 |
String( SttResId( IDS_TXTFILE ) ) ); |
1793 |
aDlg.SetCurFilter( SttResId( IDS_RESFILTER ) ); |
1794 |
} |
1795 |
|
1796 |
if ( nFileType & FT_BASIC_SOURCE ) |
1797 |
{ |
1798 |
aDlg.SetDefaultExt( String( SttResId( IDS_NONAMEFILE ) ) ); |
1799 |
aDlg.AddFilter( String( SttResId( IDS_BASFILTER ) ), |
1800 |
String( SttResId( IDS_NONAMEFILE ) ) ); |
1801 |
aDlg.AddFilter( String( SttResId( IDS_INCFILTER ) ), |
1802 |
String( SttResId( IDS_INCFILE ) ) ); |
1803 |
aDlg.SetCurFilter( SttResId( IDS_BASFILTER ) ); |
1804 |
} |
1805 |
|
1806 |
if ( nFileType & FT_BASIC_LIBRARY ) |
1807 |
{ |
90
|
|
Chapter 4 Basics of Clean C++ |
1808 |
|
aDlg.SetDefaultExt( String( SttResId( IDS_LIBFILE f) ) ); |
1809 |
|
aDlg.AddFilter( String( SttResId( IDS_LIBFILTER ) ), |
1810 |
|
String( SttResId( IDS_LIBFILE ) ) ); |
1811 |
|
aDlg.SetCurFilter( SttResId( IDS_LIBFILTER ) ); |
1812 |
|
} |
1813 |
|
|
1814 |
|
Config aConf(Config::GetConfigName( Config::GetDefDirectory(), |
1815 |
|
CUniString("testtool") )); |
1816 |
|
aConf.SetGroup( "Misc" ); |
1817 |
|
ByteString aCurrentProfile = aConf.ReadKey( "CurrentProfile", |
|
|
"Path" ); |
1818 |
|
aConf.SetGroup( aCurrentProfile ); |
1819 |
|
ByteString aFilter( aConf.ReadKey( "LastFilterName") ); |
1820 |
|
if ( aFilter.Len() ) |
1821 |
|
aDlg.SetCurFilter( String( aFilter, RTL_TEXTENCODING_UTF8 ) ); |
1822 |
|
else |
1823 |
|
aDlg.SetCurFilter( String( SttResId( IDS_BASFILTER ) ) ); |
1824 |
|
|
1825 |
|
aDlg.FilterSelect(); // Selects the last used path |
1826 |
// if ( bSave ) |
|
1827 |
|
if ( rName.Len() > 0 ) |
1828 |
|
aDlg.SetPath( rName ); |
1829 |
|
|
1830 |
|
if( aDlg.Execute() ) |
1831 |
|
{ |
1832 |
|
rName = aDlg.GetPath(); |
1833 |
/* |
rExtension = aDlg.GetCurrentFilter(); |
1834 |
|
var i:integer; |
1835 |
|
for ( i = 0 ; i < aDlg.GetFilterCount() ; i++ ) |
1836 |
|
if ( rExtension == aDlg.GetFilterName( i ) ) |
1837 |
|
rExtension = aDlg.GetFilterType( i ); |
1838 |
*/ |
|
1839 |
|
return sal_True; |
1840 |
|
} else return sal_False; |
1841 |
} |
|
91
Chapter 4 Basics of Clean C++
Question: What did you expect when you saw the member function named
QueryFileName() the first time?
Would you expect that a file selection dialog box is opened (remember the principle of least astonishment discussed in Chapter 3)? Probably not, but that is exactly what is done here. The user is obviously asked to interact with the application, so a better name for this member function would be AskUserForFilename().
But that’s not enough. If you look at the first lines in detail, you will see that there is a Boolean parameter bSave used to distinguish between a file dialog box for opening, and a file dialog box for saving files. Did you expect that? And how does the term Queryf in the function name match that fact? So, a better name for this member function may be AskUserForFilenameToOpenOrSave(). And while looking at this more expressive method name, it should immediately strike you that this method does at least two things and thus violates the single responsibility principle (discussed in detail in Chapter 6).
The following lines deal with the function’s argument nFileType. Apparently, three different file types are distinguished. The nFileType parameter is masked out with something named FT_RESULT_FILE, FT_BASIC_SOURCE, and FT_BASIC_LIBRARY. Depending on the result of this bitwise AND operation, the file dialog box is configured
differently, for example, filters are set. As the Boolean parameter bSave has done before, the three if statements introduce alternative paths. That increases what is known as the cyclomatic complexity of the function.
CYCLOMATIC COMPLEXITY
The quantitative software metric cyclomatic complexity was developed by Thomas J. McCabe, a U.S. mathematician, in 1976.
The metric is a direct count of the number of linearly independent paths through a section of source code, for example, a function. If a function contains no if or switch statement, and no for or while loop, there is just one single path through the function and its cyclomatic complexity is 1. If the function contains one if statement representing a single decision point, there are two paths through the function and the cyclomatic complexity is 2.
If cyclomatic complexity is high, the affected piece of code is typically more difficult to understand, test, and modify, and thus more prone to bugs.
92