C++ Coding Standards — Advanced
This skill extends cpp-coding-standards with concurrency, templates/concepts, standard library, enumerations, naming, performance, and the full checklist.
Concurrency & Parallelism (CP.*)
Key Rules
| Rule | Summary |
|---|
| CP.1 | Assume code will run as part of a multi-threaded program |
| CP.2 | Avoid data races |
| CP.20 | Use RAII — never lock()/unlock() directly |
| CP.21 | Use std::lock() to acquire multiple mutexes |
| CP.22 | Never call unknown code while holding a lock |
| CP.31 | Pass small amounts of data between threads by value |
| CP.32 | Don't share owning raw pointers between threads |
| CP.42 | Don't spuriously wait |
| CP.44 | Remember to name lock_guards and unique_locks |
RAII Locking
// CP.20: Use lock_guard / unique_lock — never raw lock/unlock
class BankAccount {
public:
void deposit(double amount) {
std::lock_guard<std::mutex> lock{mutex_}; // CP.44: named
balance_ += amount;
}
bool withdraw(double amount) {
std::lock_guard<std::mutex> lock{mutex_};
if (balance_ < amount) return false;
balance_ -= amount;
return true;
}
double balance() const {
std::lock_guard<std::mutex> lock{mutex_};
return balance_;
}
private:
mutable std::mutex mutex_; // mutable: locked in const functions
double balance_{0.0};
};
Multi-Mutex Lock (deadlock prevention)
// CP.21: Acquire multiple mutexes atomically
void transfer(BankAccount& from, BankAccount& to, double amount) {
// std::lock acquires both or neither — prevents deadlock
std::unique_lock<std::mutex> lock1{from.mutex_, std::defer_lock};
std::unique_lock<std::mutex> lock2{to.mutex_, std::defer_lock};
std::lock(lock1, lock2); // CP.21
from.balance_ -= amount;
to.balance_ += amount;
}
Async and Futures
// CP.31: Pass data by value between threads
std::future<int> compute_async(std::vector<int> data) {
return std::async(std::launch::async, [data = std::move(data)] {
return std::accumulate(data.begin(), data.end(), 0);
});
}
// Structured concurrency: ensure futures complete before leaving scope
void run_parallel() {
auto f1 = std::async(std::launch::async, task1);
auto f2 = std::async(std::launch::async, task2);
auto r1 = f1.get(); // Blocks until done
auto r2 = f2.get();
return r1 + r2;
}
Anti-Patterns
lock()/unlock() directly — use lock_guard (CP.20)
- Holding a lock while calling callbacks or virtual functions (CP.22)
shared_ptr for thread data — prefer value passing (CP.31/CP.32)
- Unnamed
lock_guard — dies immediately, provides no protection (CP.44)
Templates & Generic Programming (T.*)
Key Rules
| Rule | Summary |
|---|
| T.1 | Use templates to raise abstraction, not just for type parameterization |
| T.10 | Specify concepts for all template arguments |
| T.11 | Use standard concepts whenever practical |
| T.12 | Prefer concept names over auto in variable declarations |
| T.20 | Avoid "concepts" without meaningful semantics |
| T.41 | Require only essential properties in concepts |
| T.60 | Minimize template context dependence |
C++20 Concepts
#include <concepts>
// T.10: Constrain all template arguments
template<std::integral T>
T safe_add(T a, T b) {
if (b > 0 && a > std::numeric_limits<T>::max() - b)
throw std::overflow_error("overflow");
return a + b;
}
// Custom concept — T.41: require only essential properties
template<typename T>
concept Printable = requires(T t, std::ostream& os) {
{ os << t } -> std::same_as<std::ostream&>;
};
template<Printable T>
void print_all(const std::vector<T>& items) {
for (const auto& item : items)
std::cout << item << '\n';
}
// Concept composition
template<typename T>
concept Sortable = std::ranges::range<T>
&& std::totally_ordered<std::ranges::range_value_t<T>>;
template<Sortable Container>
void sort(Container& c) {
std::ranges::sort(c);
}
SFINAE vs Concepts (before/after C++20)
// Old (C++17): SFINAE — hard to read
template<typename T,
std::enable_if_t<std::is_integral_v<T>, int> = 0>
T old_style(T x) { return x * 2; }
// New (C++20): Concepts — clear and composable
template<std::integral T>
T new_style(T x) { return x * 2; }
// Alternatively with requires clause
template<typename T>
requires std::integral<T>
T also_new_style(T x) { return x * 2; }
Variadic Templates
// T.1: Raise abstraction level
template<typename... Args>
void log(std::string_view fmt, Args&&... args) {
std::cout << std::vformat(fmt, std::make_format_args(args...)) << '\n';
}
// Fold expressions (C++17)
template<typename... Ts>
auto sum(Ts... values) {
return (values + ...); // Fold expression
}
Anti-Patterns
- Unconstrained
template<typename T> where a concept applies (T.10)
- Using templates for simple runtime polymorphism (prefer virtual functions)
- Deep template nesting — extract named concepts or helper templates
Standard Library (SL.*)
Key Rules
| Rule | Summary |
|---|
| SL.1 | Use the standard library where possible |
| SL.con.1 | Prefer vector by default; use array for fixed size |
| SL.con.2 | Prefer vector over deque unless you need front insertion |
| SL.str.1 | Use std::string for owned strings |
| SL.str.2 | Use std::string_view for non-owning string references |
// SL.str.2: string_view for read-only string parameters
bool starts_with(std::string_view s, std::string_view prefix) {
return s.substr(0, prefix.size()) == prefix;
}
// SL.con.1: default to vector
std::vector<int> scores;
scores.reserve(100); // Avoid reallocations
// Fixed-size: std::array
std::array<double, 3> point{1.0, 2.0, 3.0};
// Use algorithms over raw loops
auto it = std::find_if(scores.begin(), scores.end(),
[](int s) { return s > 90; });
// C++20 ranges
auto high_scores = scores | std::views::filter([](int s) { return s > 90; })
| std::views::take(10);
Enumerations (Enum.*)
All Rules
| Rule | Summary |
|---|
| Enum.1 | Prefer enumerations over macros |
| Enum.2 | Use enumeration to represent sets of named constants |
| Enum.3 | Prefer enum class over plain enum |
| Enum.4 | Define operations on enumerations for safe use |
| Enum.5 | Don't use ALL_CAPS for enumerators |
| Enum.6 | Avoid unnamed enumerations |
| Enum.7 | Specify the underlying type only if necessary |
| Enum.8 | Specify enumerator values only if necessary |
// Enum.3: enum class prevents implicit conversion and name collision
enum class Color { Red, Green, Blue }; // NOT: RED, GREEN, BLUE (Enum.5)
// Enum.4: Define operations for safe use
Color operator+(Color c, int n) {
return static_cast<Color>(static_cast<int>(c) + n);
}
std::ostream& operator<<(std::ostream& os, Color c) {
switch (c) {
case Color::Red: return os << "Red";
case Color::Green: return os << "Green";
case Color::Blue: return os << "Blue";
}
return os << "unknown";
}
// Enum.7: Specify underlying type for serialization/interop
enum class Status : uint8_t { Active = 1, Inactive = 2, Deleted = 3 };
// DON'T: plain enum pollutes namespace
enum Direction { North, South }; // Enum.3 violation
int North = 5; // Name collision!
Source Files & Naming (SF., NL.)
Naming Conventions
| Element | Convention | Example |
|---|
| Classes/Structs | UpperCamelCase | BankAccount |
| Functions | lower_snake_case | get_balance() |
| Variables | lower_snake_case | account_id |
Constants/constexpr | ALL_CAPS or k prefix | MAX_SIZE, kMaxRetries |
| Template parameters | UpperCamelCase | template<typename ValueType> |
| Private members | trailing_underscore_ | balance_ |
| Macros | ALL_CAPS (avoid macros) | ASSERT_TRUE |
Source File Rules
| Rule | Summary |
|---|
| SF.1 | Use .cpp for code files, .h or .hpp for interfaces |
| SF.2 | Header files must not contain object definitions or non-inline function definitions |
| SF.3 | Use #pragma once or include guards |
| SF.4 | Include standard library headers before project headers |
| SF.7 | Don't write using namespace in headers |
| SF.8 | Use #include guards for all header files |
// SF.3 + SF.8: pragma once (preferred over guards)
#pragma once
// SF.4: Standard headers first
#include <string>
#include <vector>
// Then project headers
#include "myproject/core.h"
// SF.7: Never in headers
// using namespace std; // BANNED in headers
// In .cpp files, explicit is still better
// using std::string; // OK but not necessary
namespace myproject {
class Widget {
public:
explicit Widget(std::string name);
std::string name() const;
private:
std::string name_;
};
} // namespace myproject
Performance Guidelines (Per.*)
Key Rules
| Rule | Summary |
|---|
| Per.1 | Don't optimize without reason — measure first |
| Per.4 | Don't assume that complicated code is necessarily faster |
| Per.5 | Don't assume that low-level code is necessarily faster |
| Per.7 | Design to enable optimization |
| Per.10 | Rely on the static type system |
| Per.11 | Move computation from run time to compile time |
| Per.14 | Minimize the number of allocations and deallocations |
| Per.15 | Do not allocate on a critical branch |
| Per.19 | Access memory predictably (cache locality) |
// Per.11: Move to compile time
constexpr size_t fibonacci(size_t n) {
return (n <= 1) ? n : fibonacci(n-1) + fibonacci(n-2);
}
constexpr auto fib10 = fibonacci(10); // Computed at compile time
// Per.14: Minimize allocations — use reserve
std::vector<int> results;
results.reserve(expected_count); // One allocation instead of many
// Per.19: Cache-friendly access — row-major for 2D arrays
// BAD: column-major (cache unfriendly)
for (int col = 0; col < N; ++col)
for (int row = 0; row < N; ++row)
sum += matrix[row][col];
// GOOD: row-major (cache friendly)
for (int row = 0; row < N; ++row)
for (int col = 0; col < N; ++col)
sum += matrix[row][col];
// Per.10: Use types to enable compiler optimization
void process(const std::vector<int>& data) { // const enables LICM
for (int x : data) use(x);
}
Quick Reference Checklist
Philosophy & Interfaces
Functions
Classes
Resource Management
Expressions & Statements
Error Handling
Constants & Immutability
Concurrency
Templates
Enumerations
Performance