--- 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. --- # 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(size)), size_(size) {} ~Buffer() = default; Buffer(const Buffer& other) : data_(std::make_unique(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(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 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("config"); // unique ownership auto cache = std::make_shared(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 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 lock(mutex_); // CP.44: named! queue_.push(value); cv_.notify_one(); } int pop() { std::unique_lock 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 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(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 // T.10 + T.11: Constrain templates with standard concepts template 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 concept Serializable = requires(const T& t) { { t.serialize() } -> std::convertible_to; }; template 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 fixed_data{1, 2, 3, 4}; std::vector 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 #include 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 table{}; for (int i = 0; i < 256; ++i) { table[i] = i * i; } return table; }(); // Per.19: Prefer contiguous data for cache-friendliness std::vector points; // GOOD: contiguous std::vector> 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)