- _index
This is the multi-page printable view of this section. Click here to print.
Chat
- Which Languages Are Best for Multilingual Projects
- Third-party Library Pitfalls
- Design Specification Template
- Command Line Syntax Conventions
- Meanings of brackets in man pages
- Huawei C++ Coding Standards
Which Languages Are Best for Multilingual Projects
Below are 15 countries/regions—selected based on population size, economic output, and international influence—together with their language codes (shortcodes) and brief rationales, intended as a reference for multilingual translation:
Country / Region | Shortcode | Brief Rationale |
---|---|---|
United States | en-US | English is the global lingua franca; the U.S. has the world’s largest GDP (population: 333 million) and is a core market for international business and technology. |
China | zh-CN | Most populous country (1.41 billion); 2nd-largest GDP; Chinese is a UN official language; Chinese market consumption potential is enormous. |
Japan | ja-JP | Japanese is the official language of the world’s 5th-largest economy; leading in technology and manufacturing; population: 125 million with strong purchasing power. |
Germany | de-DE | Core of the Eurozone economy; largest GDP in Europe; German wields significant influence within the EU; population: 83.2 million with a robust industrial base. |
France | fr-FR | French is a UN official language; France has the 7th-largest GDP globally; population: 67.81 million; widely used in Africa and international organizations. |
India | hi-IN | Hindi is one of India’s official languages; India’s population (1.4 billion) is the world’s 2nd-largest; 6th-largest GDP and among the fastest-growing major economies. |
Spain | es-ES | Spanish has the 2nd-largest number of native speakers worldwide (548 million); Spain’s GDP is 4th in Europe, and Spanish is common throughout Latin America. |
Brazil | pt-BR | Portuguese is the native language of Brazil (population: 214 million); Brazil is South America’s largest economy with the 9th-largest GDP globally. |
South Korea | ko-KR | Korean corresponds to South Korea (population: 51.74 million); 10th-largest GDP globally; powerful in technology and cultural industries such as K-pop. |
Russia | ru-RU | Russian is a UN official language; population: 146 million; GDP ranks 11th globally; widely spoken in Central Asia and Eastern Europe. |
Italy | it-IT | Italy’s GDP is 3rd in Europe; population: 59.06 million; strong in tourism and luxury goods; Italian is an important EU language. |
Indonesia | id-ID | Indonesian is the official language of the world’s largest archipelagic nation (population: 276 million) and the largest GDP in Southeast Asia, presenting a high market potential. |
Turkey | tr-TR | Turkish is spoken by 85 million people; Turkey’s strategic position bridging Europe and Asia; GDP ranks 19th globally and exerts cultural influence in the Middle East and Central Asia. |
Netherlands | nl-NL | Dutch is spoken in the Netherlands (population: 17.5 million); GDP ranks 17th globally; leading in trade and shipping; although English penetration is high, the local market still requires the native language. |
United Arab Emirates | ar-AE | Arabic is central to the Middle East; the UAE is a Gulf economic hub (population: 9.5 million, 88 % expatriates) with well-developed oil and finance sectors, radiating influence across the Arab world. |
Notes:
Language codes follow the ISO 639-1 (language) + ISO 3166-1 (country) standards, facilitating adaptation to localization tools.
Priority has been given to countries with populations over 100 million, GDPs in the world’s top 20, and those with notable regional influence, balancing international applicability and market value.
For particular domains (e.g., the Latin American market can add es-MX (Mexico), Southeast Asia can add vi-VN (Vietnam)), the list can be further refined as needed.
Third-party Library Pitfalls
- Third-party library pitfalls
Today we talked about a vulnerability in a recently released third-party logging library that can be exploited with minimal effort to execute remote commands. A logging library and remote command execution seem completely unrelated, yet over-engineered third-party libraries are everywhere.
The more code I read, the more I feel that a lot of open-source code is of very poor quality—regardless of how many k-stars it has. Stars represent needs, not engineering skill.
The advantage of open source is that more people contribute, allowing features to grow quickly, bugs to get fixed, and code to be reviewed. But skill levels vary wildly.
Without strong commit constraints, code quality is hard to guarantee.
The more code you add, the larger the attack surface.
Although reinventing the wheel is usually bad, product requirements are like a stroller wheel: a plastic wheel that never breaks. Attaching an airplane tire to it just adds attack surface and maintenance costs. So if all you need is a stroller wheel, don’t over-engineer.
Maintenance is expensive. Third-party libraries require dedicated processes and people to maintain them. Huawei once forked a test framework, and when we upgraded the compiler the test cases failed. Upgrading the test framework clashed with the compiler upgrade, so we burned ridiculous time making further invasive changes. As one of the people involved, I deeply felt the pain of forking third-party libraries. If the modifications were feature-worthy they could be upstreamed, but tailoring them to our own needs through intrusive customization makes future maintenance nearly impossible.
Huawei created a whole series of processes for third-party libraries—one could say the friction was enormous.
The bar is set very high: adding a library requires review by a level-18 expert and approval from a level-20 director. Only longtime, well-known libraries even have a chance.
All third-party libraries live under a thirdparty folder. A full build compares them byte-for-byte with the upstream repo; any invasive change is strictly forbidden.
Dedicated tooling tracks every library’s version, managed by outsourced staff. If a developer wants to upgrade a library, they must submit a formal request—and the director has to sign off.
Getting a director to handle something like that is hard. When a process is deliberately convoluted, its real message is “please don’t do this.”
Approach third-party libraries with skepticism—trust code written by your own team.
Design Specification Template
- Design Specification Template
Detailed Design of XXX System / Sub-system
System Name | XXX System |
---|---|
Author | XXX |
— | — |
Submission Date | 2021-06-30 |
Revision History
Revised Version | Change Description | Date of Change | Author |
---|---|---|---|
v1.0 | XXXXXXX | 2021-06-30 | XXX |
— | — | — | — |
Technical Review Comments
No. | Reviewer | Review Comment (Pass/Fail/Pending, comments allowed) | Review Time |
---|---|---|---|
1 | XXX | Pass | 2022.1.1 |
Background
Glossary
- SIP: Session Initiation Protocol
- RTP: Real-time Transport Protocol
Design Objectives
Functional Requirements
Non-Functional Requirements (mandatory)
Environment
Related Software & Hardware (optional)
System Constraints
Estimated Data Scale (mandatory)
Existing Solutions
Design Ideas & Trade-offs
Assumptions & Dependencies / Relationships with Other Systems
System Design
Overview
Architecture Diagram & Explanation
System Flow & Explanation (optional)
Interfaces with External Systems
Global Data-Structure Descriptions
Brief Description of Module XXX1
Functionality of Module XXX1
Interfaces with Other Modules
Brief Description of Module XXX2
Functionality of Module XXX2
Interfaces with Other Modules
Threat Modeling
Upgrade Impact (mandatory)
Risk Assessment & Impact on Other Systems (optional)
Known or Foreseeable Risks
Potential Impact with Other Systems/Modules
Innovation Points (optional)
Attachments & References
Command Line Syntax Conventions
- Command line syntax conventions
References
- https://www.ibm.com/docs/en/iotdm/11.3?topic=interface-command-line-syntax
- https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/command-line-syntax-key
- https://developers.google.com/style/code-syntax
- https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_01
- https://ftpdocs.broadcom.com/cadocs/0/CA%20ARCserve%20%20Backup%20r16-CHS/Bookshelf_Files/HTML/cmndline/cl_cmd_line_syntax_char.htm
e.g.
Notation | Description |
---|---|
Text without brackets or braces | Items you must type as shown. |
< Text inside angle brackets> |
Placeholder for which you must supply a value. |
[ Text inside square brackets] |
Optional items. |
{ Text inside braces} |
Set of required items. You must choose one. |
Vertical bar ( | ) |
Separator for mutually exclusive items. You must choose one. |
Ellipsis (… ) |
Items that can be repeated and used multiple times. |
Meanings of brackets in man pages
- Meanings of brackets in man pages
Meanings of brackets in man pages
In command-line help, different types of brackets generally carry the following meanings:
- Angle brackets
<>
:- Angle brackets denote required arguments—values you must provide when running the command. They’re typically used to express the core syntax and parameters of a command.
- Example:
command <filename>
means you must supply a filename as a required argument, e.g.,command file.txt
.
- Square brackets
[]
:- Square brackets indicate optional arguments—values you may or may not provide when running the command. They’re commonly used to mark optional parameters and options.
- Example:
command [option]
means you can choose to provide an option, e.g.,command -v
or simplycommand
.
- Curly braces
{}
:- Curly braces usually represent a set of choices, indicating that you must select one. These are also called “choice parameter groups.”
- Example:
command {option1 | option2 | option3}
means you must pick one of the given options, e.g.,command option2
.
- Parentheses
()
:- Parentheses are generally used to group arguments, clarifying structure and precedence in a command’s syntax.
- Example:
command (option1 | option2) filename
means you must choose eitheroption1
oroption2
and supply a filename as an argument, e.g.,command option1 file.txt
.
These bracket conventions are intended to help users understand command syntax and parameter choices so they can use command-line tools correctly. When reading man pages or help text, paying close attention to the meaning and purpose of each bracket type is crucial—it prevents incorrect commands and achieves the desired results.
Huawei C++ Coding Standards
- Huawei C++ Coding Standards
C++ Language Coding Standards
Purpose
Rules are not perfect; by prohibiting features useful in specific situations, they may impact code implementation. However, the purpose of establishing rules is “to benefit the majority of developers.” If, during team collaboration, a rule is deemed unenforceable, we hope to improve it together.
Before referencing this standard, it is assumed that you already possess the corresponding basic C++ language capabilities; do not rely on this document to learn the C++ language.
- Understand the C++ language ISO standard;
- Be familiar with basic C++ language features, including those related to C++ 03/11/14/17;
- Understand the C++ standard library;
General Principles
Code must, while ensuring functional correctness, meet the feature requirements of readability, maintainability, safety, reliability, testability, efficiency, and portability.
Key Focus Areas
- Define the C++ coding style, such as naming, formatting, etc.
- C++ modular design—how to design header files, classes, interfaces, and functions.
- Best practices for C++ language features, such as constants, type casting, resource management, templates, etc.
- Modern C++ best practices, including conventions in C++11/14/17 that can improve maintainability and reliability.
- This standard is primarily applicable to C++17.
Conventions
Rule: A convention that must be followed during programming (must).
Recommendation: A convention that should be followed during programming (should).
This standard applies to common C++ standards; when no specific standard version is noted, it applies to all versions (C++03/11/14/17).
Exceptions
Regardless of ‘Rule’ or ‘Recommendation’, you must understand the reasons behind each item and strive to follow them.
However, there may be exceptions to some rules and recommendations.
If it does not violate the general principles and, after thorough consideration, there are sufficient reasons, it is acceptable to deviate from the conventions in the specification.
Exceptions break code consistency—please avoid them. Exceptions to a ‘Rule’ should be extremely rare.
In the following situations, stylistic consistency should take priority:
When modifying external open-source code or third-party code, follow the existing code style to maintain uniformity.
2 Naming
General Naming
CamelCase
Mixed case letters with words connected. Words are separated by capitalizing the first letter of each word.
Depending on whether the first letter after concatenation is capitalized, it is further divided into UpperCamelCase and lowerCamelCase.
Type | Naming Style |
---|---|
Type definitions such as classes, structs, enums, and unions, as well as scope names | UpperCamelCase |
Functions (including global, scoped, and member functions) | UpperCamelCase |
Global variables (including global and namespace-scoped variables, class static variables), local variables, function parameters, member variables of classes, structs, and unions | lowerCamelCase |
Macros, constants (const), enum values, goto labels | ALL CAPS, underscore separator |
Notes:
The constant in the above table refers to variables at global scope, namespace scope, or static member scope that are basic data types, enums, or string types qualified with const or constexpr; arrays and other types of variables are excluded.
The variable in the above table refers to all variables other than the constant definition above, which should all use the lowerCamelCase style.
File Naming
Rule 2.2.1 C++ implementation files end with .cpp, header files end with .h
We recommend using .h as the header suffix so header files can be directly compatible with both C and C++.
We recommend using .cpp as the implementation file suffix to clearly distinguish C++ code from C code.
Some other suffixes used in the industry:
- Header files: .hh, .hpp, .hxx
- cpp files: .cc, .cxx, .c
If your project team already uses a specific suffix, you can continue using it, but please maintain stylistic consistency.
For this document, we default to .h and .cpp as suffixes.
Rule 2.2.2 C++ file names correspond to the class name
C++ header and cpp file names should correspond to the class names in lowercase with underscores.
If a class is named DatabaseConnection, then the corresponding filenames should be:
- database_connection.h
- database_connection.cpp
File naming for structs, namespaces, enums, etc., follows a similar pattern.
Function Naming
Function names use the UpperCamelCase style, usually a verb or verb-object structure.
class List {
public:
void AddElement(const Element& element);
Element GetElement(const unsigned int index) const;
bool IsEmpty() const;
};
namespace Utils {
void DeleteUser();
}
Type Naming
Type names use the UpperCamelCase style.
All type names—classes, structs, unions, type aliases (typedef), and enums—use the same convention, e.g.:
// classes, structs and unions
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
union Packet { ...
// typedefs
typedef std::map<std::string, UrlTableProperties*> PropertiesMap;
// enums
enum UrlTableErrors { ...
For namespaces, UpperCamelCase is recommended:
// namespace
namespace OsUtils {
namespace FileUtils {
}
}
Recommendation 2.4.1 Avoid misusing typedef or #define to alias basic types
Do not redefine basic data types with typedef/#define unless there is a clear necessity.
Prefer using basic types from the <cstdint>
header:
Signed Type | Unsigned Type | Description |
---|---|---|
int8_t | uint8_t | Exactly 8-bit signed/unsigned integer |
int16_t | uint16_t | Exactly 16-bit signed/unsigned integer |
int32_t | uint32_t | Exactly 32-bit signed/unsigned integer |
int64_t | uint64_t | Exactly 64-bit signed/unsigned integer |
intptr_t | uintptr_t | Signed/unsigned integer to hold a pointer |
Variable Naming
General variables use lowerCamelCase, covering global variables, function parameters, local variables, and member variables.
std::string tableName; // Good: recommended
std::string tablename; // Bad: prohibited
std::string path; // Good: single-word lowercase per lowerCamelCase
Rule 2.5.1 Global variables must have a ‘g_’ prefix; static variables need no special prefix
Global variables should be used sparingly; adding the prefix visually reminds developers to use them carefully.
- Static global variables share the same naming as global variables.
- Function-local static variables share normal local variable naming.
- Class static member variables share normal member variable naming.
int g_activeConnectCount;
void Func()
{
static int packetCount = 0;
...
}
Rule 2.5.2 Class member variables are named in lowerCamelCase followed by a trailing underscore
class Foo {
private:
std::string fileName_; // trailing _ suffix, similar to K&R style
};
For struct/union member variables, use lowerCamelCase without suffix, consistent with local variables.
Macro, Constant, and Enum Naming
Macros and enum values use ALL CAPS, underscore-connected format.
Global-scope or named/anonymous-namespace const constants, as well as class static member constants, use ALL CAPS, underscore-connected; function-local const constants and ordinary const member variables use lowerCamelCase.
#define MAX(a, b) (((a) < (b)) ? (b) : (a)) // macro naming example only—macro not recommended for such a feature
enum TintColor { // enum type name in UpperCamelCase, values in ALL CAPS, underscore-connected
RED,
DARK_RED,
GREEN,
LIGHT_GREEN
};
int Func(...)
{
const unsigned int bufferSize = 100; // local constant
char *p = new char[bufferSize];
...
}
namespace Utils {
const unsigned int DEFAULT_FILE_SIZE_KB = 200; // global constant
}
3 Format
Line Length
Rule 3.1.1 Do not exceed 120 characters per line
We recommend keeping each line under 120 characters. If 120 characters are exceeded, choose a reasonable wrapping method.
Exceptions:
- Lines containing commands or URLs in comments may remain on one line for copy/paste / grep convenience, even above 120 chars.
- #include statements with long paths may exceed 120 chars but should be avoided when possible.
- Preprocessor error messages may span one line for readability, even above 120 chars.
#ifndef XXX_YYY_ZZZ
#error Header aaaa/bbbb/cccc/abc.h must only be included after xxxx/yyyy/zzzz/xyz.h, because xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#endif
Indentation
Rule 3.2.1 Use spaces for indentation—4 spaces per level
Only use spaces for indentation (4 spaces per level). Do not use tab characters.
Almost all modern IDEs can be configured to automatically expand tabs to 4 spaces—please configure yours accordingly.
Braces
Rule 3.3.1 Use K&R indentation style
K&R Style
For functions (excluding lambda expressions), place the left brace on a new line at the beginning of the line, alone; for other cases, the left brace should follow statements and stay at the end of the line.
Right braces are always on their own line, unless additional parts of the same statement follow—e.g., while in do-while, else/else-if of an if-statement, comma, or semicolon.
Examples:
struct MyType { // brace follows statement with one preceding space
...
};
int Foo(int a)
{ // function left brace on a new line, alone
if (...) {
...
} else {
...
}
}
Reason for recommending this style:
- Code is more compact.
- Continuation improves reading rhythm compared to new-line placement.
- Aligns with later language conventions and industry mainstream practice.
- Modern IDEs provide alignment aids so the end-of-line brace does not hinder scoping comprehension.
For empty function bodies, the braces may be placed on the same line:
class MyClass {
public:
MyClass() : value_(0) {}
private:
int value_;
};
Function Declarations and Definitions
Rule 3.4.1 Both return type and function name must be on the same line; break and align parameters reasonably when line limit is exceeded
When declaring or defining functions, the return type and function name must appear on the same line; if permitted by the column limit, place parameters on the same line as well—otherwise break parameters onto the next line with proper alignment.
The left parenthesis always stays on the same line as the function name; placing it on its own line is prohibited. The right parenthesis always follows the last parameter.
Examples:
ReturnType FunctionName(ArgType paramName1, ArgType paramName2) // Good: all on one line
{
...
}
ReturnType VeryVeryVeryLongFunctionName(ArgType paramName1, // line limit exceeded, break
ArgType paramName2, // Good: align with previous
ArgType paramName3)
{
...
}
ReturnType LongFunctionName(ArgType paramName1, ArgType paramName2, // line limit exceeded
ArgType paramName3, ArgType paramName4, ArgType paramName5) // Good: 4-space indent after break
{
...
}
ReturnType ReallyReallyReallyReallyLongFunctionName( // will not fit first parameter, break immediately
ArgType paramName1, ArgType paramName2, ArgType paramName3) // Good: 4-space indent after break
{
...
}
Function Calls
Rule 3.5.1 Keep function argument lists on one line. If line limit is exceeded, align parameters correctly when wrapping
Function calls should have their parameter list on one line—if exceeding the line length, break and align parameters accordingly.
Left parenthesis always follows the function name; right parenthesis always follows the last parameter.
Examples:
ReturnType result = FunctionName(paramName1, paramName2); // Good: single line
ReturnType result = FunctionName(paramName1,
paramName2, // Good: aligned with param above
paramName3);
ReturnType result = FunctionName(paramName1, paramName2,
paramName3, paramName4, paramName5); // Good: 4-space indent on break
ReturnType result = VeryVeryVeryLongFunctionName( // cannot fit first param, break immediately
paramName1, paramName2, paramName3); // 4-space indent after break
If parameters are intrinsically related, grouping them for readability may take precedence over strict formatting.
// Good: each line represents a group of related parameters
int result = DealWithStructureLikeParams(left.x, left.y, // group 1
right.x, right.y); // group 2
if Statements
Rule 3.6.1 if statements must use braces
We require braces for all if statements—even for single-line conditions.
Rationale:
- Code logic is intuitive and readable.
- Adding new code to an existing conditional is less error-prone.
- Braces protect against macro misbehavior when using functional macros (in case the macro omitted braces).
if (objectIsNotExist) { // Good: braces for single-line condition
return CreateNewObject();
}
Rule 3.6.2 Prohibit writing if/else/else if on the same line
Branches in conditional statements must appear on separate lines.
Correct:
if (someConditions) {
DoSomething();
...
} else { // Good: else on new line
...
}
Incorrect:
if (someConditions) { ... } else { ... } // Bad: else same line as if
Loops
Rule 3.7.1 Loop statements must use braces
Similar to conditions, we require braces for all for/while loops—even if the body is empty or contains only one statement.
for (int i = 0; i < someRange; i++) { // Good: braces used
DoSomething();
}
while (condition) { } // Good: empty body with braces
while (condition) {
continue; // Good: continue indicates empty logic, still braces
}
Bad examples:
for (int i = 0; i < someRange; i++)
DoSomething(); // Bad: missing braces
while (condition); // Bad: semicolon appears part of loop, confusing
switch Statements
Rule 3.8.1 Indent case/default within switch bodies one additional level
Indentation for switch statements:
switch (var) {
case 0: // Good: indented
DoSomething1(); // Good: indented
break;
case 1: { // Good: brace indentation if needed
DoSomething2();
break;
}
default:
break;
}
Bad:
switch (var) {
case 0: // Bad: case not indented
DoSomething();
break;
default: // Bad: default not indented
break;
}
Expressions
Recommendation 3.9.1 Consistently break long expressions at operators, operators stranded at EOL
When an expression is too long for one line, break at an appropriate operator. Place the operator at the end-of-line, indicating ‘to be continued’.
Example:
// Assume first line exceeds limit
if ((currentValue > threshold) && // Good: logical operator at EOL
someCondition) {
DoSomething();
...
}
int result = reallyReallyLongVariableName1 + // Good
reallyReallyLongVariableName2;
After wrapping, either align appropriately or indent subsequent lines by 4 spaces.
int sum = longVariableName1 + longVariableName2 + longVariableName3 +
longVariableName4 + longVariableName5 + longVariableName6; // Good: 4-space indent
int sum = longVariableName1 + longVariableName2 + longVariableName3 +
longVariableName4 + longVariableName5 + longVariableName6; // Good: aligned
Variable Assignment
Rule 3.10.1 Multiple variable declarations and initializations are forbidden on the same line
One variable initialization per line improves readability and comprehension.
int maxCount = 10;
bool isCompleted = false;
Bad examples:
int maxCount = 10; bool isCompleted = false; // Bad: multiple initializations must span separate lines
int x, y = 0; // Bad: multiple declarations must be on separate lines
int pointX;
int pointY;
...
pointX = 1; pointY = 2; // Bad: multiple assignments placed on same line
Exception: for loop headers, if-with-initializer (C++17), structured binding statements (C++17), etc., may declare and initialize multiple variables; forcing separation would hinder scope correctness and clarity.
Initialization
Includes initialization for structs, unions, and arrays.
Rule 3.11.1 Indent initialization lists when wrapping; align elements properly
When breaking struct/array initializers, each continuation is indented 4 spaces. Choose break and alignment points for readability.
const int rank[] = {
16, 16, 16, 16, 32, 32, 32, 32,
64, 64, 64, 64, 32, 32, 32, 32
};
Pointers and References
Recommendation 3.12.1 Place the pointer star ‘*’ adjacent to the variable or type—never add spaces on both sides or omit both
Pointer naming: align ‘*’ to either left (type) or right (variable) but never leave/pad both sides.
int* p = nullptr; // Good
int *p = nullptr; // Good
int*p = nullptr; // Bad
int * p = nullptr; // Bad
Exception when const is involved—’*’ cannot trail the variable, so avoid trailing the type:
const char * const VERSION = "V100";
Recommendation 3.12.2 Place the reference operator ‘&’ adjacent to the variable or type—never pad both sides nor omit both
Reference naming: & aligned either left (type) or right (variable); never pad both sides or omit spacing.
int i = 8;
int& p = i; // Good
int &p = i; // Good
int*& rp = pi; // Good: reference to pointer; *& together after type
int *&rp = pi; // Good: reference to pointer; *& together after variable
int* &rp = pi; // Good: pointer followed by reference—* with type, & with variable
int & p = i; // Bad
int&reeeenamespace= i; // Bad—illustrates missing space or doubled up spacing issues
Preprocessor Directives
Rule 3.13.1 Place preprocessing ‘#’ at the start of the line; nested preprocessor statements may indent ‘#’ accordingly
Preprocessing directives’ ‘#’ must be placed at the beginning of the line—even if inside function bodies.
Rule 3.13.2 Avoid macros except where necessary
Macros ignore scope, type systems, and many rules and are prone to error. Prefer non-macro approaches, and if macros must be used, ensure unique macro names.
In C++, many macro use cases can be replaced:
- Use const or enum for intuitive constants
- Use namespaces to prevent name conflicts
- Use inline functions to avoid call overhead
- Use template functions for type abstraction
Macros may be used for include guards, conditional compilation, and logging where required.
Rule 3.13.3 Do not use macros to define constants
Macros are simple text substitution completed during pre-processing; typeless, unscoped, and unsafe. Debugging errors display the value, not the macro name.
Rule 3.13.4 Do not use function-like macros
Before defining a function-like macro, consider if a real function can be used. When alternatives exist, favor functions.
Disadvantages of function-like macros:
- Lack type checking vs function calls
- Macro arguments are not evaluated (behaves differently than function calls)
- No scope encapsulation
- Heavily cryptic syntax (macro operator
#
and eternal parentheses) harms readability - Extensions needed (e.g., GCC statement expressions) hurt portability
- Compiler sees the macro after pre-processing; multi-line macro expansions collapse into one line, hard to debug or breakpoint
- Repeated expansion of large macros increases code size
Functions do not suffer the above negatives, although worst-case cost is reduced performance via call overhead (or due to micro-architecture optimization hassles).
To mitigate, use inline
. Inline functions:
- Perform strict type checking
- Evaluate each argument once
- Expand in place with no call overhead
- May optimize better than macros
For performance-critical production code, prefer inline functions over macros.
Exception:
logging macros may need to retain FILE/LINE of the call site.
Whitespace/Blank Lines
Rule 3.14.1 Horizontal spaces should emphasize keywords and key information, avoid excessive whitespace
Horizontal spaces should highlight keywords and key info; do not pad trailing spaces. General rules:
- Add space after keywords: if, switch, case, do, while, for
- Do not pad inside parentheses both-left-and-right
- Maintain symmetry around braces
- No space after unary operators (& * + - ~ !)
- Add spaces around binary operators (= + - < > * / % | & ^ <= >= == !=)
- Add spaces around ternary operators ( ? : )
- No space between pre/post inc/dec (++, –) and variable
- No space around struct member access (., ->)
- No space before comma, but space after
- No space between <> and type names as in templates or casts
- No space around scope operator ::
- Colon (:) spaced according to context when needed
Typical cases:
void Foo(int b) { // Good: space before left brace
int i = 0; // Good: spaces around =; no space before semicolon
int buf[BUF_SIZE] = {0}; // Good: no space after {
Function definition/call examples:
int result = Foo(arg1,arg2);
// ^ Bad: comma needs trailing space
int result = Foo( arg1, arg2 );
// ^ ^ Bad: no space after ( before args; none before )
Pointer/address examples:
x = *p; // Good: no space between * and p
p = &x; // Good: no space between & and x
x = r.y; // Good: no space around .
x = r->y; // Good: no space around ->
Operators:
x = 0; // Good: spaces around =
x = -5; // Good: no space between - and 5
++x; // Good: no space between ++ and x
x--;
if (x && !y) // Good: spaces around &&, none between ! and y
v = w * x + y / z; // Good: Binary operators are surrounded by spaces.
v = w * (x + z); // Good: Expressions inside parentheses are not padded with spaces.
int a = (x < y) ? x : y; // Good: Ternary operators require spaces before ? and :.
Loops and conditional statements:
if (condition) { // Good: A space after if, none inside the parentheses
...
} else { // Good: A space after else
...
}
while (condition) {} // Good: Same rules as if
for (int i = 0; i < someRange; ++i) { // Good: Space after for, after each semicolon
...
}
switch (condition) { // Good: One space between switch and (
case 0: // Good: No space between case label and colon
...
break;
...
default:
...
break;
}
Templates and casts
// Angle brackets (< and >) are never preceded by spaces, nor followed directly by spaces.
vector<string> x;
y = static_cast<char*>(x);
// One space between a type and * is acceptable; keep it consistent.
vector<char *> x;
Scope resolution operator
std::cout; // Good: No spaces around ::
int MyClass::GetValue() const {} // Good: No spaces around ::
Colons
// When spaces are required
// Good: Space before colon with class derivation
class Sub : public Base {
};
// Constructor initializer list needs spaces
MyClass::MyClass(int var) : someVar_(var)
{
DoSomething();
}
// Bit field width also gets a space
struct XX {
char a : 4;
char b : 5;
char c : 4;
};
// When spaces are NOT required
// Good: No space after public:, private:, etc.
class MyClass {
public:
MyClass(int var);
private:
int someVar_;
};
// No space after case or default in switch statements
switch (value)
{
case 1:
DoSomething();
break;
default:
break;
}
Note: Configure your IDE to strip trailing whitespace.
Advice 3.14.1 Arrange blank lines to keep code compact
Avoid needless blank lines to display more code on screen and improve readability. Observe these guidelines:
- Insert blank lines based on logical sections, not on automatic habits.
- Do not use consecutive blank lines inside functions, type definitions, macros, or initializer lists.
- Never use three or more consecutive blank lines.
- Do not leave blank lines right after a brace that opens a block or right before the closing brace; the only exception is within namespace scopes.
int Foo()
{
...
}
int Bar() // Bad: more than two consecutive blank lines.
{
...
}
if (...) {
// Bad: blank line immediately after opening brace
...
// Bad: blank line immediately before closing brace
}
int Foo(...)
{
// Bad: blank line at start of function body
...
}
Classes
Rule 3.15.1 Order class access specifiers as public:, protected:, private: at the same indentation level as class
class MyClass : public BaseClass {
public: // Note: no extra indentation
MyClass(); // standard 4-space indent
explicit MyClass(int var);
~MyClass() {}
void SomeFunction();
void SomeFunctionThatDoesNothing()
{
}
void SetVar(int var) { someVar_ = var; }
int GetVar() const { return someVar_; }
private:
bool SomeInternalFunction();
int someVar_;
int someOtherVar_;
};
Within each section group similar declarations and order them roughly as follows: type aliases (typedef, using, nested structs / classes), constants, factory functions, constructors, assignment operators, destructor, other member functions, data members.
Rule 3.15.2 Constructor initializer lists should fit on one line or be indented four spaces and line-wrapped
// If everything fits on one line:
MyClass::MyClass(int var) : someVar_(var)
{
DoSomething();
}
// Otherwise, place after the colon and indent four spaces
MyClass::MyClass(int var)
: someVar_(var), someOtherVar_(var + 1) // Good: space after comma
{
DoSomething();
}
// If multiple lines are needed, align each initializer
MyClass::MyClass(int var)
: someVar_(var), // indent 4 spaces
someOtherVar_(var + 1)
{
DoSomething();
}
4 Comments
Prefer clear architecture and naming; only add comments when necessary to aid understanding.
Keep comments concise, accurate, and non-redundant.
Comments are as important as code.
When you change code, update all relevant comments—leaving old comments is destructive.
Use English for all comments.
Comment style
Both /* */
and //
are acceptable.
Choose one consistent style for each comment category (file header, function header, inline, etc.).
Note: samples in this document frequently use trailing //
merely for explanation—do not treat it as an endorsed style.
File header
Rule 3.1 File headers must contain a valid license notice
/*
- Copyright (c) 2020 XXX
- Licensed under the Apache License, Version 2.0 (the “License”);
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an “AS IS” BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License. */
Function header comments
Rule 4.3.1 Provide headers for public functions
Callers need to know behavior, parameter ranges, return values, side effects, ownership, etc.—and the function signature alone cannot express everything.
Rule 4.3.2 Never leave noisy or empty function headers
Not every function needs a comment.
Only add a header when the signature is insufficient.
Headers appear above the declaration/definition using one of the following styles:
With //
// One-line function header
int Func1(void);
// Multi-line function header
// Second line
int Func2(void);
With /* */
/* Single-line function header */
int Func1(void);
/*
* Alternative single-line style
*/
int Func2(void);
/*
* Multi-line header
* Second line
*/
int Func3(void);
Cover: purpose, return value, performance hints, usage notes, memory ownership, re-entrancy etc.
Example:
/*
* Returns bytes actually written, -1 on failure.
* Caller owns the buffer buf.
*/
int WriteString(const char *buf, int len);
Bad:
/*
* Function name: WriteString
* Purpose: write string
* Parameters:
* Return:
*/
int WriteString(const char *buf, int len);
Problems:‐ parameters/return empty, function name redundant, unclear ownership.
Inline code comments
Rule 4.4.1 Place comments directly above or to the right of the code
Rule 4.4.2 Put one space after the comment token; right-side comments need ≥ 1 space
Above code: indent comment same as code.
Pick one consistent style.
With //
// Single-line comment
DoSomething();
// Multi-line
// comment
DoSomething();
With /* */
/* Single-line comment */
DoSomething();
/*
* Alternative multi-line comment
* second line
*/
DoSomething();
Right-of-code style: 1–4 spaces between code and token.
int foo = 100; // Right-side comment
int bar = 200; /* Right-side comment */
Aligning right-side comments is acceptable when cluster is tall:
const int A_CONST = 100; /* comments may align */
const int ANOTHER_CONST = 200; /* keep uniform gap */
If the right-side comment would exceed the line limit, move it to the previous standalone line.
Rule 4.4.3 Delete unused code, do not comment it out
Commented-out code cannot be maintained; resurrecting it later risks bugs.
When you no longer need code, remove it. Recoveries use version control.
This applies to /* */, //, #if 0, #ifdef NEVER_DEFINED, etc.
5 Headers
Responsibilities
Headers list a module’s public interface; design is reflected mostly here.
Keep implementation out of headers (inline functions are OK).
Headers should be single-minded; complexity and excessive dependencies hurt compile time.
Advice 5.1.1 Every .cpp file should have a matching .h file for the interface it provides
A .h file declares what the .cpp decides to expose.
If a .cpp publishes nothing to other TUs, it shouldn’t exist—except: program entry point, test code, or DLL edge cases.
Example:
// Foo.h
#ifndef FOO_H
#define FOO_H
class Foo {
public:
Foo();
void Fun();
private:
int value_;
};
#endif
// Foo.cpp
#include "Foo.h"
namespace { // Good: internal helper declared in .cpp, hidden by unnamed namespace or static
void Bar() {
}
}
...
void Foo::Fun()
{
Bar();
}
Header dependencies
Rule 5.2.1 Forbid circular #includes
a.h → b.h → c.h → a.h causes full rebuild on any change.
Refactor architecture to break cycles.
Rule 5.2.2 Every header must have an include guard #define
Do not use #pragma once.
Guidelines:
- Use a unique macro per file.
- No code/comments except the header license between #ifndef/#define and the matching #endif.
Example: For timer/include/timer.h:
#ifndef TIMER_INCLUDE_TIMER_H
#define TIMER_INCLUDE_TIMER_H
...
#endif
Rule 5.2.3 Forbid extern declarations of external functions/variables
Always #include
the proper header instead of duplicating signatures with extern.
Inconsistencies may appear when the real signature changes; also encourages architectural decay.
Bad: // a.cpp
extern int Fun(); // Bad
void Bar()
{
int i = Fun();
...
}
// b.cpp
int Fun() { ... }
Good: // a.cpp
#include "b.h" // Good
...
// b.h
int Fun();
// b.cpp
int Fun() { ... }
Exception: testing private members, stubs, or patches may use extern when the header itself cannot be augmented.
Rule 5.2.4 Do not #include inside extern “C”
Such includes may nest extern “C” blocks, and some compilers have limited nesting.
Likewise, C/C++ inter-op headers may silently change linkage specifiers.
Example—files a.h / b.h:
// a.h
...
#ifdef __cplusplus
void Foo(int);
#define A(value) Foo(value)
#else
void A(int)
#endif
// b.h
#ifdef __cplusplus
extern "C" {
#endif
#include "a.h"
void B();
#ifdef __cplusplus
}
#endif
After preprocessing b.h in C++:
extern "C" {
void Foo(int);
void B();
}
Foo now gets C linkage although author wanted C++ linkage.
Exception: including a pure C header lacking extern "C"
inside extern "C"
is acceptable when non-intrusive.
Advice 5.2.1 Prefer #include over forward declarations
Forward declaration quick wins:
- Saves compile time by reducing header bloat.
- Breaks needless rebuilds on unrelated header edits.
Drawbacks:
- Hides real dependencies—changes in the header may not trigger recompilation.
- Subsequent library changes can break your declaration.
- Forward declaring std:: names is undefined by C++11.
- Many declarations are longer than a single #include.
- Refactoring code just to support forward declaration often hurts performance (pointer members) and clarity.
- It is hard to decide when it is truly safe.
Hence, prefer #include
to keep dependencies explicit.
6 Scope
Namespaces
Advice 6.1.1 Use anonymous namespaces or static to hide file-local symbols
In C++03 static globals/funcs are deprecated, so unnamed namespaces are preferred.
Reasons:
- static already means too many things.
- namespace can also encapture types.
- Encourage uniform namespace usage.
- static functions cannot instantiate templates—namespaces can.
Never use anonymous namespace or static in a .h file.
// Foo.cpp
namespace {
const int MAX_COUNT = 20;
void InternalFun();
}
void Foo::Fun()
{
int i = MAX_COUNT;
InternalFun();
}
Rule 6.1.1 Never use using namespace
in headers or before #includes
Doing so pollutes every translation unit that includes it.
Example:
// a.h
namespace NamespaceA {
int Fun(int);
}
// b.h
namespace NamespaceB {
int Fun(int);
}
using namespace NamespaceB; // Bad
// a.cpp
#include "a.h"
using namespace NamespaceA;
#include "b.h" // ambiguity: NamespaceA::Fun vs NamespaceB::Fun
Allowed: bringing in single symbols or aliases inside a custom module namespace in headers:
// foo.h
#include <fancy/string>
using fancy::string; // Bad in global namespace
namespace Foo {
using fancy::string; // Good
using MyVector = fancy::vector<int>; // C++11 type alias OK
}
Global and static member functions
Advice 6.2.1 Prefer namespaces for free functions; use static members only when tightly coupled to a class
Namespace usage avoids global namespace pollution. Only when a free function is intrinsically related to a specific class should it live as a static member.
Helper logic needed only by a single .cpp belongs in an anonymous namespace.
namespace MyNamespace {
int Add(int a, int b);
}
class File {
public:
static File CreateTempFile(const std::string& fileName);
};
Global and static member constants
Advice 6.3.1 Use namespaces to hold constants; use static members only when tied to a class
Namespaces guard against global namespace clutter. Only when the constant is logically owned by a specific class should it be a static member.
Implementation-only constants belong in an anonymous namespace.
namespace MyNamespace {
const int MAX_SIZE = 100;
}
class File {
public:
static const std::string SEPARATOR;
};
Global variables
Advice 6.4.1 Minimize global variables; use a singleton instead
Mutable globals create tight, invisible coupling across TUs:
int g_counter = 0;
// a.cpp
g_counter++;
// b.cpp
g_counter++;
// c.cpp
cout << g_counter << endl;
Singleton pattern (global unique object):
class Counter {
public:
static Counter& GetInstance()
{
static Counter counter;
return counter;
}
void Increase() { value_++; }
void Print() const { std::cout << value_ << std::endl; }
private:
Counter() : value_(0) {}
int value_;
};
// a.cpp
Counter::GetInstance().Increase();
// b.cpp
Counter::GetInstance().Increase();
// c.cpp
Counter::GetInstance().Print();
This keeps state shared globally but with better encapsulation.
Exception: if the variable is module-local (each DLL/so or executable instance carries its own), you cannot use a singleton.
7 Classes
Constructors, copy, move, assignment, and destructors
These special members manage object lifetime:
- Constructor:
X()
- Copy constructor:
X(const X&)
- Copy assignment operator:
operator=(const X&)
- Move constructor:
X(X&&)
available since C++11 - Move assignment operator:
operator=(X&&)
available since C++11 - Destructor:
~X()
Rule 7.1.1 All member variables of a class must be explicitly initialized
Rationale: If a class has member variables, does not define any constructors, and lacks a default constructor, the compiler will implicitly generate one, but that generated constructor will not initialize the member variables, leaving the object in an indeterminate state.
Exceptions:
- If the member variable has a default constructor, explicit initialization is not required.
Example: The following code lacks a constructor, so the private data members are uninitialized:
class Message {
public:
void ProcessOutMsg()
{
//…
}
private:
unsigned int msgID_;
unsigned int msgLength_;
unsigned char* msgBuffer_;
std::string someIdentifier_;
};
Message message; // message members are not initialized
message.ProcessOutMsg(); // subsequent use is dangerous
// Therefore, providing a default constructor is necessary, as follows:
class Message {
public:
Message() : msgID_(0), msgLength_(0), msgBuffer_(nullptr)
{
}
void ProcessOutMsg()
{
// …
}
private:
unsigned int msgID_;
unsigned int msgLength_;
unsigned char* msgBuffer_;
std::string someIdentifier_; // has a default constructor, no explicit initialization needed
};
Advice 7.1.1 Prefer in-class member initialization (C++11) and constructor initializer lists
Rationale: C++11 in-class initialization makes the default member value obvious at a glance and should be preferred. If initialization depends on constructor parameters or C++11 is unavailable, prefer the initializer list. Compared with assigning inside the constructor body, the initializer list is more concise, performs better, and can initialize const
members and references.
class Message {
public:
Message() : msgLength_(0) // Good: prefer initializer list
{
msgBuffer_ = nullptr; // Bad: avoid assignment in constructor body
}
private:
unsigned int msgID_{0}; // Good: use C++11 in-class initialization
unsigned int msgLength_;
unsigned char* msgBuffer_;
};
Rule 7.1.2 Declare single-argument constructors explicit
to prevent implicit conversions
Rationale: A single-argument constructor not declared explicit
acts as an implicit conversion function.
Example:
class Foo {
public:
explicit Foo(const string& name): name_(name)
{
}
private:
string name_;
};
void ProcessFoo(const Foo& foo){}
int main(void)
{
std::string test = "test";
ProcessFoo(test); // compile fails
return 0;
}
Compilation fails because ProcessFoo
expects a Foo
, but a std::string
is supplied.
If explicit
were removed, the std::string
would implicitly convert into a temporary Foo
object. Such silent conversions are confusing and may hide bugs. Hence single-argument constructors must be declared explicit
.
Rule 7.1.3 Explicitly prohibit copy/move constructs/assignment when not needed
Rationale: Unless the user defines them, the compiler will generate copy constructor, copy assignment operator, move constructor and move assignment operator (move semantics are C++11+).
If the class should not support copying/moving, forbid them explicitly:
- Make the copy/move ctor or assignment operator
private
and leave it undefined:
class Foo {
private:
Foo(const Foo&);
Foo& operator=(const Foo&);
};
-
Use
= delete
from C++11, see the Modern C++ section. -
Prefer inheriting from
NoCopyable
,NoMovable
; disallow macros such asDISALLOW_COPY_AND_MOVE
,DISALLOW_COPY
,DISALLOW_MOVE
.
class Foo : public NoCopyable, public NoMovable {
};
Implementation of NoCopyable
and NoMovable
:
class NoCopyable {
public:
NoCopyable() = default;
NoCopyable(const NoCopyable&) = delete;
NoCopyable& operator = (NoCopyable&) = delete;
};
class NoMovable {
public:
NoMovable() = default;
NoMovable(NoMovable&&) noexcept = delete;
NoMovable& operator = (NoMovable&&) noexcept = delete;
};
Rule 7.1.4 Provide or prohibit both copy constructor and copy assignment together
Since both operations have copy semantics, allow or forbid them in pairs.
// Both provided
class Foo {
public:
...
Foo(const Foo&);
Foo& operator=(const Foo&);
...
};
// Both defaulted (C++11)
class Foo {
public:
Foo(const Foo&) = default;
Foo& operator=(const Foo&) = default;
};
// Both prohibited; in C++11 use `delete`
class Foo {
private:
Foo(const Foo&);
Foo& operator=(const Foo&);
};
Rule 7.1.5 Provide or prohibit both move constructor and move assignment together
Move semantics were added in C++11. If a class needs to support moving, both move constructor and move assignment must be present or both deleted.
// Both provided
class Foo {
public:
...
Foo(Foo&&);
Foo& operator=(Foo&&);
...
};
// Both defaulted
class Foo {
public:
Foo(Foo&&) = default;
Foo& operator=(Foo&&) = default;
};
// Both deleted
class Foo {
public:
Foo(Foo&&) = delete;
Foo& operator=(Foo&&) = delete;
};
Rule 7.1.6 Never call virtual functions in constructors or destructors
Rationale: Calling a virtual function on the object under construction/destruction prevents the intended polymorphic behavior.
In C++, a base class is only building one sub-object at a time.
Example:
class Base {
public:
Base();
virtual void Log() = 0; // Derived classes use different log files
};
Base::Base() // base constructor
{
Log(); // virtual call inside ctor
}
class Sub : public Base {
public:
virtual void Log();
};
When executing Sub sub;
, Sub
’s ctor runs first but calls Base()
first; during Base()
, the virtual call to Log
binds to Base::Log
, not Sub::Log
. The same applies in destructors.
Rule 7.1.7 Copy/move constructs/assignment of polymorphic bases must be non-public or deleted
Assigning a derived object to a base object causes slicing: only the base part is copied/moved, breaking polymorphism.
Negative Example:
class Base {
public:
Base() = default;
virtual ~Base() = default;
...
virtual void Fun() { std::cout << "Base" << std::endl;}
};
class Derived : public Base {
...
void Fun() override { std::cout << "Derived" << std::endl; }
};
void Foo(const Base &base)
{
Base other = base; // slicing occurs
other.Fun(); // calls Base::Fun
}
Derived d;
Foo(d); // derived passed in
Declare copy/move operations delete
or private
so the compiler rejects such assignments.
Inheritance
Rule 7.2.1 Base class destructors must be virtual
; classes not intended for inheritance should be marked final
Rationale: A base destructor must be virtual to ensure derived destructors run when the object is deleted via a base pointer.
Example: A base destructor missing virtual
causes memory leaks.
class Base {
public:
virtual std::string getVersion() = 0;
~Base()
{
std::cout << "~Base" << std::endl;
}
};
class Sub : public Base {
public:
Sub() : numbers_(nullptr)
{
}
~Sub()
{
delete[] numbers_;
std::cout << "~Sub" << std::endl;
}
int Init()
{
const size_t numberCount = 100;
numbers_ = new (std::nothrow) int[numberCount];
if (numbers_ == nullptr) {
return -1;
}
...
}
std::string getVersion()
{
return std::string("hello!");
}
private:
int* numbers_;
};
int main(int argc, char* args[])
{
Base* b = new Sub();
delete b;
return 0;
}
Because Base::~Base
is not virtual, only its destructor is invoked; Sub::~Sub
is skipped and numbers_
leaks.
Exceptions: Marker classes like NoCopyable
, NoMovable
need neither virtual destructors nor final
.
Rule 7.2.2 Virtual functions must not have default arguments
Rationale: In C++, virtual dispatch happens at runtime but default arguments are bound at compile time. The selected function body is from the derived class while its default parameter values come from the base, causing surprising behavior.
Example: the program emits “Base!” instead of the expected “Sub!”
class Base {
public:
virtual void Display(const std::string& text = "Base!")
{
std::cout << text << std::endl;
}
virtual ~Base(){}
};
class Sub : public Base {
public:
virtual void Display(const std::string& text = "Sub!")
{
std::cout << text << std::endl;
}
virtual ~Sub(){}
};
int main()
{
Base* base = new Sub();
Sub* sub = new Sub();
...
base->Display(); // prints: Base!
sub->Display(); // prints: Sub!
delete base;
delete sub;
return 0;
};
Rule 7.2.3 Do not override an inherited non-virtual function
Non-virtual functions are bound statically; only virtual functions provide dynamic dispatch.
Example:
class Base {
public:
void Fun();
};
class Sub : public Base {
public:
void Fun();
};
Sub* sub = new Sub();
Base* base = sub;
sub->Fun(); // calls Sub::Fun
base->Fun(); // calls Base::Fun
//...
Multiple Inheritance
Real-world use of multiple inheritance in our code base is rare because it brings several typical problems:
- The diamond inheritance issue causes data duplication and name ambiguity; C++ introduces virtual inheritance to address it.
- Even without diamond inheritance, name clashes between different bases can create ambiguity.
- When a subclass needs to extend/override methods in multiple bases, its responsibilities become unclear, leading to confusing semantics.
- Inheritance is white-box reuse: subclasses have access to parents’
protected
members, creating stronger coupling. Multiple inheritance compounds the coupling.
Benefits:
Multiple inheritance offers a simpler way to assemble interfaces and behaviors.
Hence multiple inheritance is allowed only in the following cases.
Advice 7.3.1 Use multiple inheritance for interface segregation and multi-role composition
If a class must implement several unrelated interfaces, inherit from separate base classes that represent those roles—similar to Scala traits.
class Role1 {};
class Role2 {};
class Role3 {};
class Object1 : public Role1, public Role2 {
// ...
};
class Object2 : public Role2, public Role3 {
// ...
};
The C++ standard library also demonstrates this pattern:
class basic_istream {};
class basic_ostream {};
class basic_iostream : public basic_istream, public basic_ostream {
};
Overloading
Overload operators only with good reason and without altering their intuitive semantics; e.g., never use ‘+’ for subtraction.
Operator overloading makes code intuitive, but also has drawbacks:
- It can mislead readers into assuming built-in efficiency and overlook potential slowdowns;
- Debugging is harder—finding a function name is easier than locating operator usage.
- Non-intuitive behaviour (e.g., ‘+’ subtracting) obfuscates code.
- Implicit conversions triggered by assignment operator overloads can hide deep bugs. Prefer functions like
Equals()
,CopyFrom()
instead of overloading==
,=
.
8 Functions
Function Design
Rule 8.1.1 Keep functions short; no more than 50 non-blank non-comment lines
A function should fit on one screen (≤ 50 lines), do one thing, and do it well.
Long functions often indicate mixed concerns, excessive complexity, or missing abstractions.
Exceptions: Algorithmic routines that are inherently cohesive and complete might exceed 50 lines.
Even if the current version works well, future changes may introduce subtle bugs. Splitting into smaller, focused functions eases readability and maintenance.
Inline Functions
Advice 8.2.1 Inline functions should be no more than 10 lines (non-blank non-comment)
Inline functions retain the usual semantics; they just expand in place. Ordinary functions incur call/return overhead; inline functions substitute the body directly.
Inlining only makes sense for very small functions (1–10 lines). For large functions the call-cost is negligible, and compilers usually fall back to normal calls.
Complex control flow (loops, switch
, try-catch
) normally precludes inlining.
Virtual functions and recursive functions cannot be inlined.
Function Parameters
Advice 8.3.1 Prefer references over pointers for parameters
References are safer: they cannot be null and cannot be reseated after binding; no null pointer checks required.
On legacy platforms follow existing conventions.
Use const
to enforce immutability and document the intent, enhancing readability.
Exception: when passing run-time sized arrays, pointers may be used.
Advice 8.3.2 Use strong typing; avoid void*
C/C++ is strongly typed; keep the style consistent. Strong type checking allows the compiler to detect mismatches early.
Using strong types prevents defects. Watch the poorly typed FooListAddNode
below:
struct FooNode {
struct List link;
int foo;
};
struct BarNode {
struct List link;
int bar;
}
void FooListAddNode(void *node) // Bad: using `void *`
{
FooNode *foo = (FooNode *)node;
ListAppend(&g_FooList, &foo->link);
}
void MakeTheList()
{
FooNode *foo = nullptr;
BarNode *bar = nullptr;
...
FooListAddNode(bar); // Wrong: meant `foo`, compiles anyway
}
- Use template functions for variant parameter types.
- Prefer polymorphic base-class pointers/ references.
Advice 8.3.3 Functions shall have no more than 5 parameters
Too many parameters increase coupling to callers and complicate testing.
If you exceed this:
- Consider splitting the function.
- Group related parameters into a single struct.
9 Additional C++ Features
Constants and Initialization
Immutable values are easier to understand, trace and analyze; default to const
for any definition.
Rule 9.1.1 Do not use macros to define constants
Macros perform simple textual substitution at preprocessing time; error traces and debuggers show raw values instead of names.
Macros lack type checking and scope.
#define MAX_MSISDN_LEN 20 // bad
// use C++ const
const int MAX_MSISDN_LEN = 20; // good
// for C++11+, prefer constexpr
constexpr int MAX_MSISDN_LEN = 20;
Advice 9.1.1 Group related integer constants using enums
Enums are safer than #define
or const int
; the compiler validates values.
// good:
enum Week {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
};
enum Color {
RED,
BLACK,
BLUE
};
void ColorizeCalendar(Week today, Color color);
ColorizeCalendar(BLUE, SUNDAY); // compile-time error: type mismatch
// poor:
const int SUNDAY = 0;
const int MONDAY = 1;
const int BLACK = 0;
const int BLUE = 1;
bool ColorizeCalendar(int today, int color);
ColorizeCalendar(BLUE, SUNDAY); // compiles fine
When enum values map to specific constants, assign them explicitly; otherwise omit assignments to avoid redundancy.
// good: device types per protocol S
enum DeviceType {
DEV_UNKNOWN = -1,
DEV_DSMP = 0,
DEV_ISMG = 1,
DEV_WAPPORTAL = 2
};
Internal enums used only for categorization should not have explicit values.
// good: session states
enum SessionState {
INIT,
CLOSED,
WAITING_FOR_RESPONSE
};
Avoid duplicate values; if unavoidable, qualify them:
enum RTCPType {
RTCP_SR = 200,
RTCP_MIN_TYPE = RTCP_SR,
RTCP_RR = 201,
RTCP_SDES = 202,
RTCP_BYE = 203,
RTCP_APP = 204,
RTCP_RTPFB = 205,
RTCP_PSFB = 206,
RTCP_XR = 207,
RTCP_RSI = 208,
RTCP_PUBPORTS = 209,
RTCP_MAX_TYPE = RTCP_PUBPORTS
};
Rule 9.1.2 Forbid magic numbers
“Magic numbers” are literals whose meaning is opaque or unclear.
Clarity is contextual: type = 12
is unclear, whereas monthsCount = yearsCount * 12
is understandable.
Even 0
can be cryptic, e.g., status = 0
.
Solution:
Comment locally used literals, or create named const
for widely used ones.
Prohibited practices:
- Names just map a macro to the literal, e.g.,
const int ZERO = 0
- Names hard-code limits, e.g.,
const int XX_TIMER_INTERVAL_300MS = 300
; useXX_TIMER_INTERVAL_MS
.
Rule 9.1.3 Each constant shall have a single responsibility
A constant must express only one concept; avoid reuse for unrelated purposes.
// good: for two protocols that both allow 20-digit MSISDNs
const unsigned int A_MAX_MSISDN_LEN = 20;
const unsigned int B_MAX_MSISDN_LEN = 20;
// or use distinct namespaces
namespace Namespace1 {
const unsigned int MAX_MSISDN_LEN = 20;
}
namespace Namespace2 {
const unsigned int MAX_MSISDN_LEN = 20;
}
Rule 9.1.4 Do not use memcpy_s
, memset_s
to initialize non-POD objects
POD (“Plain Old Data”) includes primitive types, aggregates, etc., without non-trivial default constructors, virtual functions, or base classes.
Non-POD objects (e.g., classes with virtual functions) have uncertain layouts defined by the ABI; misusing memory routines leads to undefined behavior. Even for aggregates, raw memory manipulation violates information hiding and should be avoided.
See the appendix for detailed POD specifications.
Advice 9.1.2 Declare variables at point of first use and initialize them
Using un-initialized variables is a common bug. Defining variables as late as possible and initializing immediately avoids such errors.
Declaring all variables up front leads to:
- Poor readability and maintenance: definition sites are far from usage sites.
- Variables are hard to initialize appropriately: at the beginning of a function, we often lack enough information to initialize them, so we initialize them with some empty default (e.g., zero). That is usually a waste and can also lead to errors if the variable is used before it is assigned a valid value.
Following the principle of minimizing variable scope and declaring variables as close as possible to their first use makes code easier to read and clarifies the type and initial value of every variable. In particular, initialization should be used instead of declaration followed by assignment.
// Bad: declaration and initialization are separated
string name; // default-constructed at declaration
name = "zhangsan"; // assignment operator called; definition far from declaration, harder to understand
// Good: declaration and initialization are combined, easier to understand
string name("zhangsan"); // constructor called directly
Expressions
Rule 9.2.1 Do not reference a variable again inside the same expression that increments or decrements it
C++ makes no guarantee about the order of evaluation when a variable that undergoes pre/post increment or decrement is referenced again within the same expression. Different compilers—or even different versions of the same compiler—may behave differently.
For portable code, never rely on such unspecified sequencing.
Notice that adding parentheses does not solve the problem, because this is a sequencing issue, not a precedence issue.
Example:
x = b[i] + i++; // Bad: the order of evaluating b[i] and i++ is unspecified.
Write the increment/decrement on its own line:
x = b[i] + i;
i++; // Good: on its own line
Function arguments
Func(i++, i); // Bad: cannot tell whether the increment has happened when passing the second argument
Correct:
i++; // Good: on its own line
x = Func(i, i);
Rule 9.2.2 Provide a default branch in every switch statement
In most cases, a switch statement must contain a default branch so that unlisted cases have well-defined behavior.
Exception:
If the control variable is of an enum type and all enumerators are covered by explicit case labels, a default branch is superfluous.
Modern compilers issue a warning when an enumerator in an enum is missing from the switch.
enum Color {
RED = 0,
BLUE
};
// Because the switch variable is an enum and all enumerators are covered, we can omit default
switch (color) {
case RED:
DoRedThing();
break;
case BLUE:
DoBlueThing();
...
break;
}
Suggestion 9.2.1 When writing comparisons, put the variable on the left and the constant on the right
It is unnatural to read if (MAX == v)
or even harder if (MAX > v)
.
Write the more readable form:
if (value == MAX) {
}
if (value < MAX) {
}
One exception is range checks: if (MIN < value && value < MAX)
, where the left boundary is a constant.
Do not worry about accidentally typing =
instead of ==
; compilers and static-analysis tools will warn about if (value = MAX)
. Leave typos to the tools; the code must be clear to humans.
Suggestion 9.2.2 Use parentheses to clarify operator precedence
Parentheses make the intended precedence explicit, preventing bugs caused by mismatching the default precedence rules, while simultaneously improving readability. Excessive parentheses, however, can clutter the code. Some guidance:
- If an expression contains two or more different binary operators, parenthesize it.
x = a + b + c; /* same operators, parentheses optional */
x = Foo(a + b, c); /* comma operator, no need for extra parentheses */
x = 1 << (2 + 3); /* different operators, parentheses needed */
x = a + (b / 5); /* different operators, parentheses needed */
x = (a == b) ? a : (a – b); /* different operators, parentheses needed */
Type Casting
Never customize behavior through type branching: that style is error-prone and a clear sign of writing C code in C++. It is inflexible; when a new type is added, every branch must be changed—and the compiler will not remind you. Use templates or virtual functions to let each type determine its behavior, rather than letting the calling code do it.
Avoid type casting; design types that clearly express what kind of object they represent without the need for casting. When designing a numeric type, decide:
- signed or unsigned
- float vs double
- int8, int16, int32, or int64
Inevitably, some casts remain; C++ is close to the machine and talks to APIs whose type designs are not ideal. Still, keep them to a minimum.
Exception: if a function’s return value is intentionally ignored, first reconsider the design. If ignoring it is really appropriate, cast it to (void)
.
Rule 9.3.1 Use the C++-style casts; never C-style casts
C++ casts are more specific, more readable, and safer. The C++ casts are:
-
For type conversions
dynamic_cast
— down-cast in an inheritance hierarchy and provides run-time type checking. Avoid it by improving base/derived design instead.static_cast
— like C-style but safer. Used for value conversions or up-casting, and resolving multiple-inheritance ambiguities. Prefer brace-init for pure arithmetic.reinterpret_cast
— reinterprets one type as another. Undefined behaviour potential, use sparingly.const_cast
— removes const or volatile. Suppresses immutability, leading to UB if misused.
-
Numeric conversions (C++11 onwards)
Use brace-init for converting between numeric types without loss.
double d{ someFloat };
int64_t i{ someInt32 };
Suggestion 9.3.1 Avoid dynamic_cast
dynamic_cast
depends on RTTI, letting programs discover an object’s concrete type at run time.- Its necessity usually signals a flaw in the class hierarchy; prefer redesigning classes instead.
Suggestion 9.3.2 Avoid reinterpret_cast
reinterpret_cast
performs low-level re-interpretations of memory layouts and is inherently unsafe. Avoid crossing unrelated type boundaries.
Suggestion 9.3.3 Avoid const_cast
const_cast
strips the const
and volatile
qualifiers off an object. Modifying a formerly const variable via such a cast yields Undefined Behaviour.
// Bad
const int i = 1024;
int* p = const_cast<int*>(&i);
*p = 2048; // Undefined Behaviour
// Bad
class Foo {
public:
Foo() : i(3) {}
void Fun(int v) { i = v; }
private:
int i;
};
int main() {
const Foo f;
Foo* p = const_cast<Foo*>(&f);
p->Fun(8); // Undefined Behaviour
}
Resource Acquisition and Release
Rule 9.4.1 Use delete for single objects and delete[] for arrays
- single object:
new
allocates and constructs exactly one object ⇒ dispose withdelete
. - array:
new[]
allocates space for (n) objects and constructs them ⇒ dispose withdelete[]
.
Mismatching new
/new[]
with the wrong form of delete
yields UB.
Wrong:
const int MAX_ARRAY_SIZE = 100;
int* numberArray = new int[MAX_ARRAY_SIZE];
...
delete numberArray; // ← wrong
Right:
const int MAX_ARRAY_SIZE = 100;
int* numberArray = new int[MAX_ARRAY_SIZE];
...
delete[] numberArray;
Suggestion 9.4.1 Use RAII to track dynamic resources
RAII = Resource Acquisition Is Initialization. Acquire a resource in a constructor; the destructor releases it. Benefits:
- No manual cleanup.
- The resource remains valid for the scope’s lifetime.
Example: mutex guard
class LockGuard {
public:
LockGuard(const LockType& lock) : lock_(lock) { lock_.Acquire(); }
~LockGuard() { lock_.Release(); }
private:
LockType lock_;
};
bool Update() {
LockGuard g(mutex);
...
}
Standard Library
STL usage varies between products; basic rules below.
Rule 9.5.1 Never store the pointer returned by std::string::c_str()
string::c_str()
is not guaranteed to point at stable memory. A specific implementation may return memory that is released soon after the call. Therefore, call string::c_str()
at point of use; do not store the pointer.
Example:
void Fun1() {
std::string name = "demo";
const char* text = name.c_str(); // still valid here
name = "test"; // may invalidate text
...
}
Exception: in extremely performance-critical code it is acceptable to store the pointer, provided the std::string
is guaranteed to outlive the pointer and remain unmodified while the pointer is used.
Suggestion 9.5.1 Prefer std::string over char*
Advantages:
- no manual null-termination
- built-in operators (
+
,=
,==
) - automatic memory management
Be aware: some legacy STL implementations use Copy-On-Write (COW) strings. COW is not thread-safe and can cause crashes. Passing COW strings across DLL boundaries can leave dangling references. Choose a modern, non-COW STL when possible.
Exception: APIs that require 'char*'
get it from std::string::c_str()
. Stack buffers should be plain char arrays, not std::string
or std::vector<char>
.
Rule 9.5.2 Do not use auto_ptr
std::auto_ptr
performs implicit (and surprising) ownership transfer:
auto_ptr<T> p1(new T);
auto_ptr<T> p2 = p1; // p1 becomes nullptr
Therefore it is forbidden.
Use std::unique_ptr
for exclusive ownership, std::shared_ptr
for shared ownership.
Suggestion 9.5.2 Prefer canonical C++ headers
Use <cstdlib>
instead of <stdlib.h>
, etc.
const Qualifier
Add const
to variables and parameters whose values must not change. Mark member functions const
if they do not modify the observable state.
Rule 9.6.1 Use const for pointer/reference parameters that are read-only
Write:
void PrintFoo(const Foo& foo);
instead of Foo&
.
Rule 9.6.2 Mark member functions const when they do not modify the object
Accessors should always be const.
class Foo {
public:
int PrintValue() const;
int GetValue() const;
private:
int value_;
};
Suggestion 9.6.1 Declare member variables const when they never change after initialization
class Foo {
public:
Foo(int length) : dataLength_(length) {}
private:
const int dataLength_;
};
Exceptions
Suggestion 9.7.1 Mark functions that never throw as noexcept (C++11)
Benefits:
- Enables better code generation.
- Standard containers only use move-construction when a move operator is noexcept. Without
noexcept
they fall back to (slower) copy-construction.
Example:
extern "C" double sqrt(double) noexcept;
std::vector<int> Compute(const std::vector<int>& v) noexcept {
...
}
Destructor, default constructors, swap
, and move operations must never throw.
Templates & Generic Programming
Rule 9.8.1 Prohibit generic programming in the OpenHarmony project
Generic programming, templates, and OOP are driven by entirely different ideas and techniques. OpenHarmony is mainly based on OOP.
Avoid templates because:
- Inexperienced developers tend to produce bloated, confusing code.
- Template code is hard to read and debug.
- Error messages are notoriously cryptic.
- Templates may generate excessive code size.
- Refactoring template code is difficult because its instantiations are spread across the codebase.
OpenHarmony forbids template programming in most modules.
Exception: STL adaptation layers may still use templates.
Macros
Avoid complex macros. Instead:
- Use
const
or enums for constants. - Replace macro functions with inline or template functions.
// Bad
#define SQUARE(a, b) ((a) * (b))
// Prefer
template<typename T>
T Square(T a, T b) { return a * b; }
10 Modern C++ Features
ISO standardized C++11 in 2011 and C++17 in March 2017. These standards add countless improvements. This chapter highlights best practices for using them effectively.
Brevity & Safety
Suggestion 10.1.1 Use auto judiciously
- Eliminates long type names, guarantees initialization.
- Deduction rules are subtle—understand them.
- Prefer explicit types if clarity improves. Use
auto
only for local variables.
Examples:
auto iter = m.find(val);
auto p = new Foo;
int x; // uninitialized
auto x; // compile-time error—uninitialized
Beware deduced types:
std::vector<std::string> v;
auto s1 = v[0]; // std::string, makes a copy
Rule 10.1.1 Override virtual functions with override or final
They ensure correctness: the compiler rejects overrides whose signatures do not match the base declaration.
class Base {
virtual void Foo();
};
class Derived : public Base {
void Foo() override; // OK
void Foo() const override; // Error: signature differs
};
Rule 10.1.2 Delete functions with the delete keyword
Clearer and broader than making members private and undefined.
Foo& operator=(const Foo&) = delete;
Rule 10.1.3 Prefer nullptr to NULL or 0
nullptr
has its own type (std::nullptr_t
) and unambiguous behaviour; 0/NULL cannot.
Or, when a null pointer is required, directly using 0
can introduce another problem: the code becomes unclear—especially when auto
is used:
auto result = Find(id);
if (result == 0) { // Does Find() return a pointer or an integer?
// do something
}
Literally, 0
is of type int
(0L
is long
), so neither NULL
nor 0
is of a pointer type.
When a function is overloaded for both pointer and integer types, passing NULL
or 0
will invoke the integer overload:
void F(int);
void F(int*);
F(0); // Calls F(int), not F(int*)
F(NULL); // Calls F(int), not F(int*)
Moreover, sizeof(NULL) == sizeof(void*)
is not necessarily true, which is another potential pitfall.
Summary: directly using 0
or 0L
makes the code unclear and type-unsafe; using NULL
is no better than 0
because it is also type-unsafe. All of them involve potential risks.
The advantage of nullptr
is not just being a literal representation of the null pointer that clarifies the code, but also that it is definitively not an integer type.
nullptr
is of type std::nullptr_t
, and std::nullptr_t
can be implicitly converted to any raw pointer type, so nullptr
can act as the null pointer for any type.
void F(int);
void F(int*);
F(nullptr); // Calls F(int*)
auto result = Find(id);
if (result == nullptr) { // Find() returns a pointer
// do something
}
Rule 10.1.4: Prefer using
over typedef
Prior to C++11, you could define type aliases with typedef
; nobody wants to repeatedly type std::map<uint32_t, std::vector<int>>
.
typedef std::map<uint32_t, std::vector<int>> SomeType;
An alias is essentially an encapsulation of the real type. This encapsulation makes code clearer and prevents shotgun surgery when the underlying type changes.
Since C++11, using
provides alias declarations:
using SomeType = std::map<uint32_t, std::vector<int>>;
Compare their syntaxes:
typedef Type Alias; // Is Type first or Alias first?
using Alias = Type; // Reads like assignment—intuitive and error-proof
If that alone isn’t enough to adopt using
, look at alias templates:
// Alias template definition—one line
template<class T>
using MyAllocatorVector = std::vector<T, MyAllocator<T>>;
MyAllocatorVector<int> data; // Using the alias
template<class T>
class MyClass {
private:
MyAllocatorVector<int> data_; // Alias usable inside a template
};
typedef
does not support template parameters in aliases; workarounds are required:
// Need to wrap typedef in a template struct
template<class T>
struct MyAllocatorVector {
typedef std::vector<T, MyAllocator<T>> type;
};
MyAllocatorVector<int>::type data; // Using typedef—must append ::type
template<class T>
class MyClass {
private:
typename MyAllocatorVector<int>::type data_; // Need typename too
};
Rule 10.1.5: Do not apply std::move
to const objects
Semantically, std::move
is about moving an object. A const object cannot be modified and is therefore immovable, so applying std::move
confuses readers.
Functionally, std::move
yields an rvalue reference; for const objects this becomes const&&
. Very few types provide move constructors or move-assignment operators taking const&&
, so the operation usually degrades to a copy, harming performance.
Bad:
std::string g_string;
std::vector<std::string> g_stringList;
void func()
{
const std::string myString = "String content";
g_string = std::move(myString); // Bad: copies, does not move
const std::string anotherString = "Another string content";
g_stringList.push_back(std::move(anotherString)); // Bad: also copies
}
Smart Pointers
Rule 10.2.1: Prefer raw pointers for singletons, data members, etc., whose ownership is never shared
Rationale
Smart pointers prevent leaks by automatically releasing resources but add overhead—code bloat, extra construction/destruction cost, increased memory footprint, etc.
For objects whose ownership is never transferred (singletons, data members), simply deleting them in the destructor is sufficient; avoid the extra smart-pointer overhead.
Example
class Foo;
class Base {
public:
Base() {}
virtual ~Base()
{
delete foo_;
}
private:
Foo* foo_ = nullptr;
};
Exceptions
- When returning newly created objects that need custom deleters, smart pointers are acceptable.
class User;
class Foo {
public:
std::unique_ptr<User, void(User *)> CreateUniqueUser()
{
sptr<User> ipcUser = iface_cast<User>(remoter);
return std::unique_ptr<User, void(User *)>(::new User(ipcUser), [](User *user) {
user->Close();
::delete user;
});
}
std::shared_ptr<User> CreateSharedUser()
{
sptr<User> ipcUser = iface_cast<User>(remoter);
return std::shared_ptr<User>(ipcUser.GetRefPtr(), [ipcUser](User *user) mutable {
ipcUser = nullptr;
});
}
};
- Use
shared_ptr
when returning a newly created object that can have multiple owners.
Rule 10.2.2: Use std::make_unique
, not new
, to create a unique_ptr
Rationale
make_unique
is more concise.- It provides exception safety in complex expressions.
Example
// Bad: type appears twice—possible inconsistency
std::unique_ptr<MyClass> ptr(new MyClass(0, 1));
// Good: type appears only once
auto ptr = std::make_unique<MyClass>(0, 1);
Repetition can cause subtle bugs:
// Compiles, but mismatched new[] and delete
std::unique_ptr<uint8_t> ptr(new uint8_t[10]);
std::unique_ptr<uint8_t[]> ptr(new uint8_t);
// Not exception-safe: evaluation order may be
// 1. allocate Foo
// 2. construct Foo
// 3. call Bar
// 4. construct unique_ptr<Foo>
// If Bar throws, Foo leaks.
F(unique_ptr<Foo>(new Foo()), Bar());
// Exception-safe: no interleaving
F(make_unique<Foo>(), Bar());
Exception
std::make_unique
does not support custom deleters.
Fall back to new
only when a custom deleter is required.
Rule 10.2.4: Use std::make_shared
, not new
, to create a shared_ptr
Rationale
Besides the same consistency benefits as make_unique
, make_shared
offers performance gains.
A shared_ptr
manages two entities:
- Control block (ref-count, deleter, etc.)
- The owned object
make_shared
allocates one heap block for both, whereas
std::shared_ptr<MyClass>(new MyClass)
performs two allocations: one for MyClass
and one for the control block, adding overhead.
Exception
Like make_unique
, make_shared
cannot accept a custom deleter.
Lambda Expressions
Advice 10.3.1: Prefer lambda expressions when a function cannot express what you need (capture locals, local function)
Rationale
Functions cannot capture locals nor be declared in local scope. When you need such features, use lambda rather than hand-written functors.
Conversely, lambdas and functors can’t be overloaded; overloadable cases favor functions.
When both lambdas and functions work, prefer functions—always reach for the simplest tool.
Example
// Overloads for int and string—natural to choose
void F(int);
void F(const string&);
// Needed: capture locals or appear inline
vector<Work> v = LotsOfWork();
for (int taskNum = 0; taskNum < max; ++taskNum) {
pool.Run([=, &v] {...});
}
pool.Join();
Rule 10.3.1: Return or store lambdas outside local scope only by value capture; never by reference
Rationale
A non-local lambda (returned, stored on heap, passed to another thread) must not hold dangling references; avoid capture by reference.
Example
// Bad
void Foo()
{
int local = 42;
// Capture by reference; `local` dangles after return
threadPool.QueueWork([&]{ Process(local); });
}
// Good
void Foo()
{
int local = 42;
// Capture by value
threadPool.QueueWork([=]{ Process(local); });
}
Advice 10.3.2: If you capture this
, write all other captures explicitly
Rationale
Inside a member function [=]
looks like capture-by-copy, but it implicitly captures this
by copy, yielding handles to every data member (i.e., reference semantics in disguise). When you really want that, write it explicitly.
Example
class MyClass {
public:
void Foo()
{
int i = 0;
auto Lambda = [=]() { Use(i, data_); }; // Bad: not a true copy of data_
data_ = 42;
Lambda(); // Uses 42
data_ = 43;
Lambda(); // Uses 43; shows reference semantics
auto Lambda2 = [i, this]() { Use(i, data_); }; // Good: explicit, no surprises
}
private:
int data_ = 0;
};
Advice 10.3.3: Avoid default capture modes
Rationale
Lambdas support default-by-reference (&
) and default-by-value (=
).
Default-by-reference silently binds every local variable; easy to create dangling refs.
Default-by-value implicitly captures this
and hides which variables are actually used; readers may mistakenly believe static variables are copied too.
Therefore always list the captures explicitly.
Bad
auto func()
{
int addend = 5;
static int baseValue = 3;
return [=]() { // only copies addend
++baseValue; // modifies global
return baseValue + addend;
};
}
Good
auto func()
{
int addend = 5;
static int baseValue = 3;
return [addend, baseValue = baseValue]() mutable { // C++14 capture-init
++baseValue; // modifies local copy
return baseValue + addend;
};
}
Reference: Effective Modern C++, Item 31: Avoid default capture modes.
Interfaces
Advice 10.4.1: In interfaces not concerned with ownership, pass T*
or T&
, not smart pointers
Rationale
- Smart pointers transfer or share ownership only when needed.
- Requiring smart pointers forces callers to use them (e.g., impossible to pass
this
). - Passing shared-ownership smart pointers incurs runtime overhead.
Example
// Accepts any int*
void F(int*);
// Accepts only when ownership is to be transferred
void G(unique_ptr<int>);
// Accepts only when ownership is to be shared
void G(shared_ptr<int>);
// Ownership unchanged, but caller must hold a unique_ptr
void H(const unique_ptr<int>&);
// Accepts any int
void H(int&);
// Bad
void F(shared_ptr<Widget>& w)
{
// ...
Use(*w); // lifetime not relevant
// ...
}