724 lines
22 KiB
Markdown
724 lines
22 KiB
Markdown
|
|
---
|
||
|
|
name: cpp-coding-standards
|
||
|
|
description: C++ coding standards based on the C++ Core Guidelines (isocpp.github.io). Use when writing, reviewing, or refactoring C++ code to enforce modern, safe, and idiomatic practices.
|
||
|
|
origin: ECC
|
||
|
|
---
|
||
|
|
|
||
|
|
# C++ Coding Standards (C++ Core Guidelines)
|
||
|
|
|
||
|
|
Comprehensive coding standards for modern C++ (C++17/20/23) derived from the [C++ Core Guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines). Enforces type safety, resource safety, immutability, and clarity.
|
||
|
|
|
||
|
|
## When to Use
|
||
|
|
|
||
|
|
- Writing new C++ code (classes, functions, templates)
|
||
|
|
- Reviewing or refactoring existing C++ code
|
||
|
|
- Making architectural decisions in C++ projects
|
||
|
|
- Enforcing consistent style across a C++ codebase
|
||
|
|
- Choosing between language features (e.g., `enum` vs `enum class`, raw pointer vs smart pointer)
|
||
|
|
|
||
|
|
### When NOT to Use
|
||
|
|
|
||
|
|
- Non-C++ projects
|
||
|
|
- Legacy C codebases that cannot adopt modern C++ features
|
||
|
|
- Embedded/bare-metal contexts where specific guidelines conflict with hardware constraints (adapt selectively)
|
||
|
|
|
||
|
|
## Cross-Cutting Principles
|
||
|
|
|
||
|
|
These themes recur across the entire guidelines and form the foundation:
|
||
|
|
|
||
|
|
1. **RAII everywhere** (P.8, R.1, E.6, CP.20): Bind resource lifetime to object lifetime
|
||
|
|
2. **Immutability by default** (P.10, Con.1-5, ES.25): Start with `const`/`constexpr`; mutability is the exception
|
||
|
|
3. **Type safety** (P.4, I.4, ES.46-49, Enum.3): Use the type system to prevent errors at compile time
|
||
|
|
4. **Express intent** (P.3, F.1, NL.1-2, T.10): Names, types, and concepts should communicate purpose
|
||
|
|
5. **Minimize complexity** (F.2-3, ES.5, Per.4-5): Simple code is correct code
|
||
|
|
6. **Value semantics over pointer semantics** (C.10, R.3-5, F.20, CP.31): Prefer returning by value and scoped objects
|
||
|
|
|
||
|
|
## Philosophy & Interfaces (P.*, I.*)
|
||
|
|
|
||
|
|
### Key Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **P.1** | Express ideas directly in code |
|
||
|
|
| **P.3** | Express intent |
|
||
|
|
| **P.4** | Ideally, a program should be statically type safe |
|
||
|
|
| **P.5** | Prefer compile-time checking to run-time checking |
|
||
|
|
| **P.8** | Don't leak any resources |
|
||
|
|
| **P.10** | Prefer immutable data to mutable data |
|
||
|
|
| **I.1** | Make interfaces explicit |
|
||
|
|
| **I.2** | Avoid non-const global variables |
|
||
|
|
| **I.4** | Make interfaces precisely and strongly typed |
|
||
|
|
| **I.11** | Never transfer ownership by a raw pointer or reference |
|
||
|
|
| **I.23** | Keep the number of function arguments low |
|
||
|
|
|
||
|
|
### DO
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// P.10 + I.4: Immutable, strongly typed interface
|
||
|
|
struct Temperature {
|
||
|
|
double kelvin;
|
||
|
|
};
|
||
|
|
|
||
|
|
Temperature boil(const Temperature& water);
|
||
|
|
```
|
||
|
|
|
||
|
|
### DON'T
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// Weak interface: unclear ownership, unclear units
|
||
|
|
double boil(double* temp);
|
||
|
|
|
||
|
|
// Non-const global variable
|
||
|
|
int g_counter = 0; // I.2 violation
|
||
|
|
```
|
||
|
|
|
||
|
|
## Functions (F.*)
|
||
|
|
|
||
|
|
### Key Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **F.1** | Package meaningful operations as carefully named functions |
|
||
|
|
| **F.2** | A function should perform a single logical operation |
|
||
|
|
| **F.3** | Keep functions short and simple |
|
||
|
|
| **F.4** | If a function might be evaluated at compile time, declare it `constexpr` |
|
||
|
|
| **F.6** | If your function must not throw, declare it `noexcept` |
|
||
|
|
| **F.8** | Prefer pure functions |
|
||
|
|
| **F.16** | For "in" parameters, pass cheaply-copied types by value and others by `const&` |
|
||
|
|
| **F.20** | For "out" values, prefer return values to output parameters |
|
||
|
|
| **F.21** | To return multiple "out" values, prefer returning a struct |
|
||
|
|
| **F.43** | Never return a pointer or reference to a local object |
|
||
|
|
|
||
|
|
### Parameter Passing
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// F.16: Cheap types by value, others by const&
|
||
|
|
void print(int x); // cheap: by value
|
||
|
|
void analyze(const std::string& data); // expensive: by const&
|
||
|
|
void transform(std::string s); // sink: by value (will move)
|
||
|
|
|
||
|
|
// F.20 + F.21: Return values, not output parameters
|
||
|
|
struct ParseResult {
|
||
|
|
std::string token;
|
||
|
|
int position;
|
||
|
|
};
|
||
|
|
|
||
|
|
ParseResult parse(std::string_view input); // GOOD: return struct
|
||
|
|
|
||
|
|
// BAD: output parameters
|
||
|
|
void parse(std::string_view input,
|
||
|
|
std::string& token, int& pos); // avoid this
|
||
|
|
```
|
||
|
|
|
||
|
|
### Pure Functions and constexpr
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// F.4 + F.8: Pure, constexpr where possible
|
||
|
|
constexpr int factorial(int n) noexcept {
|
||
|
|
return (n <= 1) ? 1 : n * factorial(n - 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
static_assert(factorial(5) == 120);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Anti-Patterns
|
||
|
|
|
||
|
|
- Returning `T&&` from functions (F.45)
|
||
|
|
- Using `va_arg` / C-style variadics (F.55)
|
||
|
|
- Capturing by reference in lambdas passed to other threads (F.53)
|
||
|
|
- Returning `const T` which inhibits move semantics (F.49)
|
||
|
|
|
||
|
|
## Classes & Class Hierarchies (C.*)
|
||
|
|
|
||
|
|
### Key Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **C.2** | Use `class` if invariant exists; `struct` if data members vary independently |
|
||
|
|
| **C.9** | Minimize exposure of members |
|
||
|
|
| **C.20** | If you can avoid defining default operations, do (Rule of Zero) |
|
||
|
|
| **C.21** | If you define or `=delete` any copy/move/destructor, handle them all (Rule of Five) |
|
||
|
|
| **C.35** | Base class destructor: public virtual or protected non-virtual |
|
||
|
|
| **C.41** | A constructor should create a fully initialized object |
|
||
|
|
| **C.46** | Declare single-argument constructors `explicit` |
|
||
|
|
| **C.67** | A polymorphic class should suppress public copy/move |
|
||
|
|
| **C.128** | Virtual functions: specify exactly one of `virtual`, `override`, or `final` |
|
||
|
|
|
||
|
|
### Rule of Zero
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// C.20: Let the compiler generate special members
|
||
|
|
struct Employee {
|
||
|
|
std::string name;
|
||
|
|
std::string department;
|
||
|
|
int id;
|
||
|
|
// No destructor, copy/move constructors, or assignment operators needed
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Rule of Five
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// C.21: If you must manage a resource, define all five
|
||
|
|
class Buffer {
|
||
|
|
public:
|
||
|
|
explicit Buffer(std::size_t size)
|
||
|
|
: data_(std::make_unique<char[]>(size)), size_(size) {}
|
||
|
|
|
||
|
|
~Buffer() = default;
|
||
|
|
|
||
|
|
Buffer(const Buffer& other)
|
||
|
|
: data_(std::make_unique<char[]>(other.size_)), size_(other.size_) {
|
||
|
|
std::copy_n(other.data_.get(), size_, data_.get());
|
||
|
|
}
|
||
|
|
|
||
|
|
Buffer& operator=(const Buffer& other) {
|
||
|
|
if (this != &other) {
|
||
|
|
auto new_data = std::make_unique<char[]>(other.size_);
|
||
|
|
std::copy_n(other.data_.get(), other.size_, new_data.get());
|
||
|
|
data_ = std::move(new_data);
|
||
|
|
size_ = other.size_;
|
||
|
|
}
|
||
|
|
return *this;
|
||
|
|
}
|
||
|
|
|
||
|
|
Buffer(Buffer&&) noexcept = default;
|
||
|
|
Buffer& operator=(Buffer&&) noexcept = default;
|
||
|
|
|
||
|
|
private:
|
||
|
|
std::unique_ptr<char[]> data_;
|
||
|
|
std::size_t size_;
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Class Hierarchy
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// C.35 + C.128: Virtual destructor, use override
|
||
|
|
class Shape {
|
||
|
|
public:
|
||
|
|
virtual ~Shape() = default;
|
||
|
|
virtual double area() const = 0; // C.121: pure interface
|
||
|
|
};
|
||
|
|
|
||
|
|
class Circle : public Shape {
|
||
|
|
public:
|
||
|
|
explicit Circle(double r) : radius_(r) {}
|
||
|
|
double area() const override { return 3.14159 * radius_ * radius_; }
|
||
|
|
|
||
|
|
private:
|
||
|
|
double radius_;
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Anti-Patterns
|
||
|
|
|
||
|
|
- Calling virtual functions in constructors/destructors (C.82)
|
||
|
|
- Using `memset`/`memcpy` on non-trivial types (C.90)
|
||
|
|
- Providing different default arguments for virtual function and overrider (C.140)
|
||
|
|
- Making data members `const` or references, which suppresses move/copy (C.12)
|
||
|
|
|
||
|
|
## Resource Management (R.*)
|
||
|
|
|
||
|
|
### Key Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **R.1** | Manage resources automatically using RAII |
|
||
|
|
| **R.3** | A raw pointer (`T*`) is non-owning |
|
||
|
|
| **R.5** | Prefer scoped objects; don't heap-allocate unnecessarily |
|
||
|
|
| **R.10** | Avoid `malloc()`/`free()` |
|
||
|
|
| **R.11** | Avoid calling `new` and `delete` explicitly |
|
||
|
|
| **R.20** | Use `unique_ptr` or `shared_ptr` to represent ownership |
|
||
|
|
| **R.21** | Prefer `unique_ptr` over `shared_ptr` unless sharing ownership |
|
||
|
|
| **R.22** | Use `make_shared()` to make `shared_ptr`s |
|
||
|
|
|
||
|
|
### Smart Pointer Usage
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// R.11 + R.20 + R.21: RAII with smart pointers
|
||
|
|
auto widget = std::make_unique<Widget>("config"); // unique ownership
|
||
|
|
auto cache = std::make_shared<Cache>(1024); // shared ownership
|
||
|
|
|
||
|
|
// R.3: Raw pointer = non-owning observer
|
||
|
|
void render(const Widget* w) { // does NOT own w
|
||
|
|
if (w) w->draw();
|
||
|
|
}
|
||
|
|
|
||
|
|
render(widget.get());
|
||
|
|
```
|
||
|
|
|
||
|
|
### RAII Pattern
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// R.1: Resource acquisition is initialization
|
||
|
|
class FileHandle {
|
||
|
|
public:
|
||
|
|
explicit FileHandle(const std::string& path)
|
||
|
|
: handle_(std::fopen(path.c_str(), "r")) {
|
||
|
|
if (!handle_) throw std::runtime_error("Failed to open: " + path);
|
||
|
|
}
|
||
|
|
|
||
|
|
~FileHandle() {
|
||
|
|
if (handle_) std::fclose(handle_);
|
||
|
|
}
|
||
|
|
|
||
|
|
FileHandle(const FileHandle&) = delete;
|
||
|
|
FileHandle& operator=(const FileHandle&) = delete;
|
||
|
|
FileHandle(FileHandle&& other) noexcept
|
||
|
|
: handle_(std::exchange(other.handle_, nullptr)) {}
|
||
|
|
FileHandle& operator=(FileHandle&& other) noexcept {
|
||
|
|
if (this != &other) {
|
||
|
|
if (handle_) std::fclose(handle_);
|
||
|
|
handle_ = std::exchange(other.handle_, nullptr);
|
||
|
|
}
|
||
|
|
return *this;
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
std::FILE* handle_;
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Anti-Patterns
|
||
|
|
|
||
|
|
- Naked `new`/`delete` (R.11)
|
||
|
|
- `malloc()`/`free()` in C++ code (R.10)
|
||
|
|
- Multiple resource allocations in a single expression (R.13 -- exception safety hazard)
|
||
|
|
- `shared_ptr` where `unique_ptr` suffices (R.21)
|
||
|
|
|
||
|
|
## Expressions & Statements (ES.*)
|
||
|
|
|
||
|
|
### Key Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **ES.5** | Keep scopes small |
|
||
|
|
| **ES.20** | Always initialize an object |
|
||
|
|
| **ES.23** | Prefer `{}` initializer syntax |
|
||
|
|
| **ES.25** | Declare objects `const` or `constexpr` unless modification is intended |
|
||
|
|
| **ES.28** | Use lambdas for complex initialization of `const` variables |
|
||
|
|
| **ES.45** | Avoid magic constants; use symbolic constants |
|
||
|
|
| **ES.46** | Avoid narrowing/lossy arithmetic conversions |
|
||
|
|
| **ES.47** | Use `nullptr` rather than `0` or `NULL` |
|
||
|
|
| **ES.48** | Avoid casts |
|
||
|
|
| **ES.50** | Don't cast away `const` |
|
||
|
|
|
||
|
|
### Initialization
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// ES.20 + ES.23 + ES.25: Always initialize, prefer {}, default to const
|
||
|
|
const int max_retries{3};
|
||
|
|
const std::string name{"widget"};
|
||
|
|
const std::vector<int> primes{2, 3, 5, 7, 11};
|
||
|
|
|
||
|
|
// ES.28: Lambda for complex const initialization
|
||
|
|
const auto config = [&] {
|
||
|
|
Config c;
|
||
|
|
c.timeout = std::chrono::seconds{30};
|
||
|
|
c.retries = max_retries;
|
||
|
|
c.verbose = debug_mode;
|
||
|
|
return c;
|
||
|
|
}();
|
||
|
|
```
|
||
|
|
|
||
|
|
### Anti-Patterns
|
||
|
|
|
||
|
|
- Uninitialized variables (ES.20)
|
||
|
|
- Using `0` or `NULL` as pointer (ES.47 -- use `nullptr`)
|
||
|
|
- C-style casts (ES.48 -- use `static_cast`, `const_cast`, etc.)
|
||
|
|
- Casting away `const` (ES.50)
|
||
|
|
- Magic numbers without named constants (ES.45)
|
||
|
|
- Mixing signed and unsigned arithmetic (ES.100)
|
||
|
|
- Reusing names in nested scopes (ES.12)
|
||
|
|
|
||
|
|
## Error Handling (E.*)
|
||
|
|
|
||
|
|
### Key Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **E.1** | Develop an error-handling strategy early in a design |
|
||
|
|
| **E.2** | Throw an exception to signal that a function can't perform its assigned task |
|
||
|
|
| **E.6** | Use RAII to prevent leaks |
|
||
|
|
| **E.12** | Use `noexcept` when throwing is impossible or unacceptable |
|
||
|
|
| **E.14** | Use purpose-designed user-defined types as exceptions |
|
||
|
|
| **E.15** | Throw by value, catch by reference |
|
||
|
|
| **E.16** | Destructors, deallocation, and swap must never fail |
|
||
|
|
| **E.17** | Don't try to catch every exception in every function |
|
||
|
|
|
||
|
|
### Exception Hierarchy
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// E.14 + E.15: Custom exception types, throw by value, catch by reference
|
||
|
|
class AppError : public std::runtime_error {
|
||
|
|
public:
|
||
|
|
using std::runtime_error::runtime_error;
|
||
|
|
};
|
||
|
|
|
||
|
|
class NetworkError : public AppError {
|
||
|
|
public:
|
||
|
|
NetworkError(const std::string& msg, int code)
|
||
|
|
: AppError(msg), status_code(code) {}
|
||
|
|
int status_code;
|
||
|
|
};
|
||
|
|
|
||
|
|
void fetch_data(const std::string& url) {
|
||
|
|
// E.2: Throw to signal failure
|
||
|
|
throw NetworkError("connection refused", 503);
|
||
|
|
}
|
||
|
|
|
||
|
|
void run() {
|
||
|
|
try {
|
||
|
|
fetch_data("https://api.example.com");
|
||
|
|
} catch (const NetworkError& e) {
|
||
|
|
log_error(e.what(), e.status_code);
|
||
|
|
} catch (const AppError& e) {
|
||
|
|
log_error(e.what());
|
||
|
|
}
|
||
|
|
// E.17: Don't catch everything here -- let unexpected errors propagate
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Anti-Patterns
|
||
|
|
|
||
|
|
- Throwing built-in types like `int` or string literals (E.14)
|
||
|
|
- Catching by value (slicing risk) (E.15)
|
||
|
|
- Empty catch blocks that silently swallow errors
|
||
|
|
- Using exceptions for flow control (E.3)
|
||
|
|
- Error handling based on global state like `errno` (E.28)
|
||
|
|
|
||
|
|
## Constants & Immutability (Con.*)
|
||
|
|
|
||
|
|
### All Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **Con.1** | By default, make objects immutable |
|
||
|
|
| **Con.2** | By default, make member functions `const` |
|
||
|
|
| **Con.3** | By default, pass pointers and references to `const` |
|
||
|
|
| **Con.4** | Use `const` for values that don't change after construction |
|
||
|
|
| **Con.5** | Use `constexpr` for values computable at compile time |
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// Con.1 through Con.5: Immutability by default
|
||
|
|
class Sensor {
|
||
|
|
public:
|
||
|
|
explicit Sensor(std::string id) : id_(std::move(id)) {}
|
||
|
|
|
||
|
|
// Con.2: const member functions by default
|
||
|
|
const std::string& id() const { return id_; }
|
||
|
|
double last_reading() const { return reading_; }
|
||
|
|
|
||
|
|
// Only non-const when mutation is required
|
||
|
|
void record(double value) { reading_ = value; }
|
||
|
|
|
||
|
|
private:
|
||
|
|
const std::string id_; // Con.4: never changes after construction
|
||
|
|
double reading_{0.0};
|
||
|
|
};
|
||
|
|
|
||
|
|
// Con.3: Pass by const reference
|
||
|
|
void display(const Sensor& s) {
|
||
|
|
std::cout << s.id() << ": " << s.last_reading() << '\n';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Con.5: Compile-time constants
|
||
|
|
constexpr double PI = 3.14159265358979;
|
||
|
|
constexpr int MAX_SENSORS = 256;
|
||
|
|
```
|
||
|
|
|
||
|
|
## Concurrency & Parallelism (CP.*)
|
||
|
|
|
||
|
|
### Key Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **CP.2** | Avoid data races |
|
||
|
|
| **CP.3** | Minimize explicit sharing of writable data |
|
||
|
|
| **CP.4** | Think in terms of tasks, rather than threads |
|
||
|
|
| **CP.8** | Don't use `volatile` for synchronization |
|
||
|
|
| **CP.20** | Use RAII, never plain `lock()`/`unlock()` |
|
||
|
|
| **CP.21** | Use `std::scoped_lock` to acquire multiple mutexes |
|
||
|
|
| **CP.22** | Never call unknown code while holding a lock |
|
||
|
|
| **CP.42** | Don't wait without a condition |
|
||
|
|
| **CP.44** | Remember to name your `lock_guard`s and `unique_lock`s |
|
||
|
|
| **CP.100** | Don't use lock-free programming unless you absolutely have to |
|
||
|
|
|
||
|
|
### Safe Locking
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// CP.20 + CP.44: RAII locks, always named
|
||
|
|
class ThreadSafeQueue {
|
||
|
|
public:
|
||
|
|
void push(int value) {
|
||
|
|
std::lock_guard<std::mutex> lock(mutex_); // CP.44: named!
|
||
|
|
queue_.push(value);
|
||
|
|
cv_.notify_one();
|
||
|
|
}
|
||
|
|
|
||
|
|
int pop() {
|
||
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
||
|
|
// CP.42: Always wait with a condition
|
||
|
|
cv_.wait(lock, [this] { return !queue_.empty(); });
|
||
|
|
const int value = queue_.front();
|
||
|
|
queue_.pop();
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
std::mutex mutex_; // CP.50: mutex with its data
|
||
|
|
std::condition_variable cv_;
|
||
|
|
std::queue<int> queue_;
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Multiple Mutexes
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// CP.21: std::scoped_lock for multiple mutexes (deadlock-free)
|
||
|
|
void transfer(Account& from, Account& to, double amount) {
|
||
|
|
std::scoped_lock lock(from.mutex_, to.mutex_);
|
||
|
|
from.balance_ -= amount;
|
||
|
|
to.balance_ += amount;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Anti-Patterns
|
||
|
|
|
||
|
|
- `volatile` for synchronization (CP.8 -- it's for hardware I/O only)
|
||
|
|
- Detaching threads (CP.26 -- lifetime management becomes nearly impossible)
|
||
|
|
- Unnamed lock guards: `std::lock_guard<std::mutex>(m);` destroys immediately (CP.44)
|
||
|
|
- Holding locks while calling callbacks (CP.22 -- deadlock risk)
|
||
|
|
- Lock-free programming without deep expertise (CP.100)
|
||
|
|
|
||
|
|
## Templates & Generic Programming (T.*)
|
||
|
|
|
||
|
|
### Key Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **T.1** | Use templates to raise the level of abstraction |
|
||
|
|
| **T.2** | Use templates to express algorithms for many argument types |
|
||
|
|
| **T.10** | Specify concepts for all template arguments |
|
||
|
|
| **T.11** | Use standard concepts whenever possible |
|
||
|
|
| **T.13** | Prefer shorthand notation for simple concepts |
|
||
|
|
| **T.43** | Prefer `using` over `typedef` |
|
||
|
|
| **T.120** | Use template metaprogramming only when you really need to |
|
||
|
|
| **T.144** | Don't specialize function templates (overload instead) |
|
||
|
|
|
||
|
|
### Concepts (C++20)
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
#include <concepts>
|
||
|
|
|
||
|
|
// T.10 + T.11: Constrain templates with standard concepts
|
||
|
|
template<std::integral T>
|
||
|
|
T gcd(T a, T b) {
|
||
|
|
while (b != 0) {
|
||
|
|
a = std::exchange(b, a % b);
|
||
|
|
}
|
||
|
|
return a;
|
||
|
|
}
|
||
|
|
|
||
|
|
// T.13: Shorthand concept syntax
|
||
|
|
void sort(std::ranges::random_access_range auto& range) {
|
||
|
|
std::ranges::sort(range);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Custom concept for domain-specific constraints
|
||
|
|
template<typename T>
|
||
|
|
concept Serializable = requires(const T& t) {
|
||
|
|
{ t.serialize() } -> std::convertible_to<std::string>;
|
||
|
|
};
|
||
|
|
|
||
|
|
template<Serializable T>
|
||
|
|
void save(const T& obj, const std::string& path);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Anti-Patterns
|
||
|
|
|
||
|
|
- Unconstrained templates in visible namespaces (T.47)
|
||
|
|
- Specializing function templates instead of overloading (T.144)
|
||
|
|
- Template metaprogramming where `constexpr` suffices (T.120)
|
||
|
|
- `typedef` instead of `using` (T.43)
|
||
|
|
|
||
|
|
## Standard Library (SL.*)
|
||
|
|
|
||
|
|
### Key Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **SL.1** | Use libraries wherever possible |
|
||
|
|
| **SL.2** | Prefer the standard library to other libraries |
|
||
|
|
| **SL.con.1** | Prefer `std::array` or `std::vector` over C arrays |
|
||
|
|
| **SL.con.2** | Prefer `std::vector` by default |
|
||
|
|
| **SL.str.1** | Use `std::string` to own character sequences |
|
||
|
|
| **SL.str.2** | Use `std::string_view` to refer to character sequences |
|
||
|
|
| **SL.io.50** | Avoid `endl` (use `'\n'` -- `endl` forces a flush) |
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// SL.con.1 + SL.con.2: Prefer vector/array over C arrays
|
||
|
|
const std::array<int, 4> fixed_data{1, 2, 3, 4};
|
||
|
|
std::vector<std::string> dynamic_data;
|
||
|
|
|
||
|
|
// SL.str.1 + SL.str.2: string owns, string_view observes
|
||
|
|
std::string build_greeting(std::string_view name) {
|
||
|
|
return "Hello, " + std::string(name) + "!";
|
||
|
|
}
|
||
|
|
|
||
|
|
// SL.io.50: Use '\n' not endl
|
||
|
|
std::cout << "result: " << value << '\n';
|
||
|
|
```
|
||
|
|
|
||
|
|
## Enumerations (Enum.*)
|
||
|
|
|
||
|
|
### Key Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **Enum.1** | Prefer enumerations over macros |
|
||
|
|
| **Enum.3** | Prefer `enum class` over plain `enum` |
|
||
|
|
| **Enum.5** | Don't use ALL_CAPS for enumerators |
|
||
|
|
| **Enum.6** | Avoid unnamed enumerations |
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// Enum.3 + Enum.5: Scoped enum, no ALL_CAPS
|
||
|
|
enum class Color { red, green, blue };
|
||
|
|
enum class LogLevel { debug, info, warning, error };
|
||
|
|
|
||
|
|
// BAD: plain enum leaks names, ALL_CAPS clashes with macros
|
||
|
|
enum { RED, GREEN, BLUE }; // Enum.3 + Enum.5 + Enum.6 violation
|
||
|
|
#define MAX_SIZE 100 // Enum.1 violation -- use constexpr
|
||
|
|
```
|
||
|
|
|
||
|
|
## Source Files & Naming (SF.*, NL.*)
|
||
|
|
|
||
|
|
### Key Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **SF.1** | Use `.cpp` for code files and `.h` for interface files |
|
||
|
|
| **SF.7** | Don't write `using namespace` at global scope in a header |
|
||
|
|
| **SF.8** | Use `#include` guards for all `.h` files |
|
||
|
|
| **SF.11** | Header files should be self-contained |
|
||
|
|
| **NL.5** | Avoid encoding type information in names (no Hungarian notation) |
|
||
|
|
| **NL.8** | Use a consistent naming style |
|
||
|
|
| **NL.9** | Use ALL_CAPS for macro names only |
|
||
|
|
| **NL.10** | Prefer `underscore_style` names |
|
||
|
|
|
||
|
|
### Header Guard
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// SF.8: Include guard (or #pragma once)
|
||
|
|
#ifndef PROJECT_MODULE_WIDGET_H
|
||
|
|
#define PROJECT_MODULE_WIDGET_H
|
||
|
|
|
||
|
|
// SF.11: Self-contained -- include everything this header needs
|
||
|
|
#include <string>
|
||
|
|
#include <vector>
|
||
|
|
|
||
|
|
namespace project::module {
|
||
|
|
|
||
|
|
class Widget {
|
||
|
|
public:
|
||
|
|
explicit Widget(std::string name);
|
||
|
|
const std::string& name() const;
|
||
|
|
|
||
|
|
private:
|
||
|
|
std::string name_;
|
||
|
|
};
|
||
|
|
|
||
|
|
} // namespace project::module
|
||
|
|
|
||
|
|
#endif // PROJECT_MODULE_WIDGET_H
|
||
|
|
```
|
||
|
|
|
||
|
|
### Naming Conventions
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// NL.8 + NL.10: Consistent underscore_style
|
||
|
|
namespace my_project {
|
||
|
|
|
||
|
|
constexpr int max_buffer_size = 4096; // NL.9: not ALL_CAPS (it's not a macro)
|
||
|
|
|
||
|
|
class tcp_connection { // underscore_style class
|
||
|
|
public:
|
||
|
|
void send_message(std::string_view msg);
|
||
|
|
bool is_connected() const;
|
||
|
|
|
||
|
|
private:
|
||
|
|
std::string host_; // trailing underscore for members
|
||
|
|
int port_;
|
||
|
|
};
|
||
|
|
|
||
|
|
} // namespace my_project
|
||
|
|
```
|
||
|
|
|
||
|
|
### Anti-Patterns
|
||
|
|
|
||
|
|
- `using namespace std;` in a header at global scope (SF.7)
|
||
|
|
- Headers that depend on inclusion order (SF.10, SF.11)
|
||
|
|
- Hungarian notation like `strName`, `iCount` (NL.5)
|
||
|
|
- ALL_CAPS for anything other than macros (NL.9)
|
||
|
|
|
||
|
|
## Performance (Per.*)
|
||
|
|
|
||
|
|
### Key Rules
|
||
|
|
|
||
|
|
| Rule | Summary |
|
||
|
|
|------|---------|
|
||
|
|
| **Per.1** | Don't optimize without reason |
|
||
|
|
| **Per.2** | Don't optimize prematurely |
|
||
|
|
| **Per.6** | Don't make claims about performance without measurements |
|
||
|
|
| **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.19** | Access memory predictably |
|
||
|
|
|
||
|
|
### Guidelines
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
// Per.11: Compile-time computation where possible
|
||
|
|
constexpr auto lookup_table = [] {
|
||
|
|
std::array<int, 256> table{};
|
||
|
|
for (int i = 0; i < 256; ++i) {
|
||
|
|
table[i] = i * i;
|
||
|
|
}
|
||
|
|
return table;
|
||
|
|
}();
|
||
|
|
|
||
|
|
// Per.19: Prefer contiguous data for cache-friendliness
|
||
|
|
std::vector<Point> points; // GOOD: contiguous
|
||
|
|
std::vector<std::unique_ptr<Point>> indirect_points; // BAD: pointer chasing
|
||
|
|
```
|
||
|
|
|
||
|
|
### Anti-Patterns
|
||
|
|
|
||
|
|
- Optimizing without profiling data (Per.1, Per.6)
|
||
|
|
- Choosing "clever" low-level code over clear abstractions (Per.4, Per.5)
|
||
|
|
- Ignoring data layout and cache behavior (Per.19)
|
||
|
|
|
||
|
|
## Quick Reference Checklist
|
||
|
|
|
||
|
|
Before marking C++ work complete:
|
||
|
|
|
||
|
|
- [ ] No raw `new`/`delete` -- use smart pointers or RAII (R.11)
|
||
|
|
- [ ] Objects initialized at declaration (ES.20)
|
||
|
|
- [ ] Variables are `const`/`constexpr` by default (Con.1, ES.25)
|
||
|
|
- [ ] Member functions are `const` where possible (Con.2)
|
||
|
|
- [ ] `enum class` instead of plain `enum` (Enum.3)
|
||
|
|
- [ ] `nullptr` instead of `0`/`NULL` (ES.47)
|
||
|
|
- [ ] No narrowing conversions (ES.46)
|
||
|
|
- [ ] No C-style casts (ES.48)
|
||
|
|
- [ ] Single-argument constructors are `explicit` (C.46)
|
||
|
|
- [ ] Rule of Zero or Rule of Five applied (C.20, C.21)
|
||
|
|
- [ ] Base class destructors are public virtual or protected non-virtual (C.35)
|
||
|
|
- [ ] Templates are constrained with concepts (T.10)
|
||
|
|
- [ ] No `using namespace` in headers at global scope (SF.7)
|
||
|
|
- [ ] Headers have include guards and are self-contained (SF.8, SF.11)
|
||
|
|
- [ ] Locks use RAII (`scoped_lock`/`lock_guard`) (CP.20)
|
||
|
|
- [ ] Exceptions are custom types, thrown by value, caught by reference (E.14, E.15)
|
||
|
|
- [ ] `'\n'` instead of `std::endl` (SL.io.50)
|
||
|
|
- [ ] No magic numbers (ES.45)
|