From WeChat Official Account: DeepNoMind
Author: Yu Fan
C++ code offers sufficient flexibility, making it challenging for most engineers to grasp. This article introduces ten best practices to follow for writing good C++ code and provides a tool at the end to help analyze the robustness of C++ code. Original text: 10 Best practices to design and implement a C++ class
1. Try to Use the Latest C++ Standards Whenever Possible
As of 2022, C++ has been around for over 40 years. The new C++ standards actually simplify many frustrating details and provide modern approaches to improve C++ code, but it is not easy for developers to recognize this.
For example, memory management is one of the most criticized mechanisms in C++. For years, object allocation has been done using the new keyword, and developers must remember to call delete somewhere in the code. “Modern C++” addresses this issue and promotes the use of smart pointers.
2. Use Namespaces to Modularize Code
Modern C++ libraries widely use namespaces to modularize codebases, employing a “Namespace-by-feature” approach that divides namespaces by functionality, grouping everything related to a single feature (and only that feature) into a single namespace. This results in high cohesion and modularity of namespaces, with minimal coupling, as tightly coupled projects are grouped together.
Boost is the best example of feature-based grouping, containing thousands of namespaces, each used to group specific features.
3. Abstraction
Data abstraction is one of the most fundamental and important features of object-oriented programming in C++. Abstraction means showing only the essential information while hiding the details; data abstraction refers to providing only the basic information about data to the outside world while hiding background details or implementations.
Despite many books, online resources, conference speakers, and experts recommending this best practice, it is still often ignored in many projects, with many class details not being hidden.
4. Keep Classes as Small as Possible
Types with multiple lines of code should be divided into a set of smaller types.
Refactoring a large class requires significant patience and may even necessitate recreating everything from scratch. Here are some refactoring suggestions:
- The logic in BigClass must be divided into smaller classes. These smaller classes may eventually become private classes nested within the original God Class, with instances of the God Class composed of instances of the smaller nested classes.
- The division of smaller classes should be driven by multiple responsibilities handled by the God Class. To determine these responsibilities, it is often necessary to look for a subset of methods that are strongly coupled with a subset of fields.
- If BigClass contains more logic than state, a good option is to define one or several static classes that contain only pure static methods without static fields. Pure static methods are functions that compute results based solely on input parameters without reading or allocating any static or instance fields. The main advantage of pure static methods is their ease of testing.
- First, try to maintain the interface of BigClass and delegate calls to the newly extracted classes. Ultimately, BigClass should be a pure interface without its own logic, which can be kept for convenience or discarded in favor of using only the new classes.
- Unit tests can help: write tests for each method before extracting methods to ensure functionality is not broken.
5. Provide the Minimum Number of Methods for Each Class
Classes with more than 20 methods can be difficult to understand and maintain.
A class with many methods may be a symptom of having too many responsibilities.
Perhaps the class in question controls too many other classes in the system and has exceeded its intended logic, becoming a god class.
6. Strengthen Low Coupling
Low coupling is an ideal state where fewer changes are needed in the application to implement a change in the program. In the long run, this can significantly reduce the time, effort, and cost of modifying and adding new features.
Low coupling can be achieved by using abstract classes or generic classes and methods.
7. Strengthen High Cohesion
The Single Responsibility Principle states that a class should not have more than one reason to change; such classes are called cohesive classes. A higher LCOM value often indicates poor cohesion of the class. There are several LCOM metrics, ranging from [0-1]. LCOM HS (HS stands for Henderson-Sellers) ranges from [0-2]. An LCOM HS value greater than 1 should raise a warning. Here is how to calculate the LCOM metric:
LCOM = 1 — (sum(MF)/M*F) LCOM HS = (M — sum(MF)/F)(M-1)
Where…
- M is the number of methods in the class (including static methods and instance methods, it also includes constructors, property getters/setters, event add/remove methods).
- F is the number of instance fields in the class.
- MF is the number of methods that access a specific instance field.
- Sum(MF) is the sum of MF for all instance fields of the class.
The basic idea behind these formulas can be stated as follows: If all methods of a class use all its instance fields, then the class is fully cohesive, meaning sum(MF)=M*F, and thus LCOM = 0 and LCOMHS = 0.
LCOMHS values greater than 1 should raise a warning.
8. Comment Only on What the Code Cannot Express
Redundant code comments do not provide any additional value to the reader. Codebases are often filled with noisy comments and incorrect comments, prompting programmers to ignore all comments or take active measures to hide them.
9. Avoid Duplicate Code as Much as Possible
It is well known that the presence of duplicate code has a negative impact on software development and maintenance. In fact, a major drawback is that when changing instances of duplicate code to fix bugs or add new features, all corresponding code must be changed simultaneously.
The most common cause of duplicate code is copy/paste operations, where similar source code appears in two or more places. Many articles, books, and websites warn against this practice, but sometimes it is not easy to implement these recommendations, and developers still opt for the simple solution: the copy/paste method.
Using appropriate tools can easily detect duplicate code from copy/paste operations; however, in some cases, cloned code can be difficult to detect.
10. Immutability Aids Multithreaded Programming
Essentially, if an object’s state does not change after it is created, then that object is immutable. If an instance of a class is immutable, then that class is immutable.
Immutable objects greatly simplify concurrent programming, which is a significant reason for their use. Consider why writing proper multithreaded programs is a daunting task? Because synchronizing thread access to resources (objects or other operating system resources) is challenging. Why is synchronizing these accesses difficult? Because it is hard to ensure that multiple threads do not encounter race conditions during multiple write and read accesses to multiple objects. What if there were no write accesses? In other words, what if the state of the object being accessed by threads did not change? Then synchronization would no longer be necessary!
Another benefit of immutable classes is that they never violate the Liskov Substitution Principle (LSP). Here is the definition of LSP from the encyclopedia:
The concept of behavioral subtyping defined by Liskov establishes the concept of substitutability of mutable objects, meaning that if S is a subtype of T, then objects of type T in a program can be replaced with objects of type S without altering any of the expected properties of that program (e.g., correctness).
If there are no public fields, no methods that can change their internal data, and derived class methods cannot change their internal data, then the reference object class is immutable. Because the value is immutable, the same object can be referenced in all cases without needing copy constructors or assignment operators. For this reason, it is recommended to make copy constructors and assignment operators private, inherit from boost::noncopyable, or use the new C++11 feature of “explicitly defaulted and deleted special member functions”[2].
How to Strengthen Checks on These Best Practices?
CppDepend[3] provides a code query language called CQLinq[4] that allows querying codebases like a database. Developers, designers, and architects can customize queries to easily find situations prone to bugs.
With CQLinq, it is possible to define very advanced queries that combine data from code metrics, dependencies, API usage, and other models to match situations prone to bugs.
For example, after analyzing the clang source code, large classes can be detected:

Classes with a large number of methods can be detected:

Or classes with poor cohesion can be detected:

References:[1] 10 Best practices to design and implement a C++ class: https://issamvb.medium.com/10-best-practices-to-design-and-implement-a-c-class-4326611827e1#:~:text=10%20Best%20practices%20to%20design%20and%20implement%20a,class%20as%20you%20can.%20…%20More%20items…%20[2] Explicitly defaulted and deleted special member functions: http://en.wikipedia.org/wiki/C%2B%2B11#Explicitly_defaulted_and_deleted_special_member_functions[3] CppDepend: http://www.cppdepend.com/[4] CQLinq: https://www.cppdepend.com/cqlinq
Recommended↓↓↓