C++ Coding Standards (C++ Core Guidelines)
Comprehensive coding standards for modern C++ (C++17/20/23) derived from the C++ Core Guidelines. 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:
- RAII everywhere (P.8, R.1, E.6, CP.20): Bind resource lifetime to object lifetime
- Immutability by default (P.10, Con.1-5, ES.25): Start with
const/constexpr; mutability is the exception
- Type safety (P.4, I.4, ES.46-49, Enum.3): Use the type system to prevent errors at compile time
- Express intent (P.3, F.1, NL.1-2, T.10): Names, types, and concepts should communicate purpose
- Minimize complexity (F.2-3, ES.5, Per.4-5): Simple code is correct code
- 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
1// P.10 + I.4: Immutable, strongly typed interface
2struct Temperature {
3 double kelvin;
4};
5
6Temperature boil(const Temperature& water);
DON'T
cpp
1// Weak interface: unclear ownership, unclear units
2double boil(double* temp);
3
4// Non-const global variable
5int 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
1// F.16: Cheap types by value, others by const&
2void print(int x); // cheap: by value
3void analyze(const std::string& data); // expensive: by const&
4void transform(std::string s); // sink: by value (will move)
5
6// F.20 + F.21: Return values, not output parameters
7struct ParseResult {
8 std::string token;
9 int position;
10};
11
12ParseResult parse(std::string_view input); // GOOD: return struct
13
14// BAD: output parameters
15void parse(std::string_view input,
16 std::string& token, int& pos); // avoid this
Pure Functions and constexpr
cpp
1// F.4 + F.8: Pure, constexpr where possible
2constexpr int factorial(int n) noexcept {
3 return (n <= 1) ? 1 : n * factorial(n - 1);
4}
5
6static_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
1// C.20: Let the compiler generate special members
2struct Employee {
3 std::string name;
4 std::string department;
5 int id;
6 // No destructor, copy/move constructors, or assignment operators needed
7};
Rule of Five
cpp
1// C.21: If you must manage a resource, define all five
2class Buffer {
3public:
4 explicit Buffer(std::size_t size)
5 : data_(std::make_unique<char[]>(size)), size_(size) {}
6
7 ~Buffer() = default;
8
9 Buffer(const Buffer& other)
10 : data_(std::make_unique<char[]>(other.size_)), size_(other.size_) {
11 std::copy_n(other.data_.get(), size_, data_.get());
12 }
13
14 Buffer& operator=(const Buffer& other) {
15 if (this != &other) {
16 auto new_data = std::make_unique<char[]>(other.size_);
17 std::copy_n(other.data_.get(), other.size_, new_data.get());
18 data_ = std::move(new_data);
19 size_ = other.size_;
20 }
21 return *this;
22 }
23
24 Buffer(Buffer&&) noexcept = default;
25 Buffer& operator=(Buffer&&) noexcept = default;
26
27private:
28 std::unique_ptr<char[]> data_;
29 std::size_t size_;
30};
Class Hierarchy
cpp
1// C.35 + C.128: Virtual destructor, use override
2class Shape {
3public:
4 virtual ~Shape() = default;
5 virtual double area() const = 0; // C.121: pure interface
6};
7
8class Circle : public Shape {
9public:
10 explicit Circle(double r) : radius_(r) {}
11 double area() const override { return 3.14159 * radius_ * radius_; }
12
13private:
14 double radius_;
15};
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_ptrs |
Smart Pointer Usage
cpp
1// R.11 + R.20 + R.21: RAII with smart pointers
2auto widget = std::make_unique<Widget>("config"); // unique ownership
3auto cache = std::make_shared<Cache>(1024); // shared ownership
4
5// R.3: Raw pointer = non-owning observer
6void render(const Widget* w) { // does NOT own w
7 if (w) w->draw();
8}
9
10render(widget.get());
RAII Pattern
cpp
1// R.1: Resource acquisition is initialization
2class FileHandle {
3public:
4 explicit FileHandle(const std::string& path)
5 : handle_(std::fopen(path.c_str(), "r")) {
6 if (!handle_) throw std::runtime_error("Failed to open: " + path);
7 }
8
9 ~FileHandle() {
10 if (handle_) std::fclose(handle_);
11 }
12
13 FileHandle(const FileHandle&) = delete;
14 FileHandle& operator=(const FileHandle&) = delete;
15 FileHandle(FileHandle&& other) noexcept
16 : handle_(std::exchange(other.handle_, nullptr)) {}
17 FileHandle& operator=(FileHandle&& other) noexcept {
18 if (this != &other) {
19 if (handle_) std::fclose(handle_);
20 handle_ = std::exchange(other.handle_, nullptr);
21 }
22 return *this;
23 }
24
25private:
26 std::FILE* handle_;
27};
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
1// ES.20 + ES.23 + ES.25: Always initialize, prefer {}, default to const
2const int max_retries{3};
3const std::string name{"widget"};
4const std::vector<int> primes{2, 3, 5, 7, 11};
5
6// ES.28: Lambda for complex const initialization
7const auto config = [&] {
8 Config c;
9 c.timeout = std::chrono::seconds{30};
10 c.retries = max_retries;
11 c.verbose = debug_mode;
12 return c;
13}();
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
1// E.14 + E.15: Custom exception types, throw by value, catch by reference
2class AppError : public std::runtime_error {
3public:
4 using std::runtime_error::runtime_error;
5};
6
7class NetworkError : public AppError {
8public:
9 NetworkError(const std::string& msg, int code)
10 : AppError(msg), status_code(code) {}
11 int status_code;
12};
13
14void fetch_data(const std::string& url) {
15 // E.2: Throw to signal failure
16 throw NetworkError("connection refused", 503);
17}
18
19void run() {
20 try {
21 fetch_data("https://api.example.com");
22 } catch (const NetworkError& e) {
23 log_error(e.what(), e.status_code);
24 } catch (const AppError& e) {
25 log_error(e.what());
26 }
27 // E.17: Don't catch everything here -- let unexpected errors propagate
28}
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
1// Con.1 through Con.5: Immutability by default
2class Sensor {
3public:
4 explicit Sensor(std::string id) : id_(std::move(id)) {}
5
6 // Con.2: const member functions by default
7 const std::string& id() const { return id_; }
8 double last_reading() const { return reading_; }
9
10 // Only non-const when mutation is required
11 void record(double value) { reading_ = value; }
12
13private:
14 const std::string id_; // Con.4: never changes after construction
15 double reading_{0.0};
16};
17
18// Con.3: Pass by const reference
19void display(const Sensor& s) {
20 std::cout << s.id() << ": " << s.last_reading() << '\n';
21}
22
23// Con.5: Compile-time constants
24constexpr double PI = 3.14159265358979;
25constexpr 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_guards and unique_locks |
| CP.100 | Don't use lock-free programming unless you absolutely have to |
Safe Locking
cpp
1// CP.20 + CP.44: RAII locks, always named
2class ThreadSafeQueue {
3public:
4 void push(int value) {
5 std::lock_guard<std::mutex> lock(mutex_); // CP.44: named!
6 queue_.push(value);
7 cv_.notify_one();
8 }
9
10 int pop() {
11 std::unique_lock<std::mutex> lock(mutex_);
12 // CP.42: Always wait with a condition
13 cv_.wait(lock, [this] { return !queue_.empty(); });
14 const int value = queue_.front();
15 queue_.pop();
16 return value;
17 }
18
19private:
20 std::mutex mutex_; // CP.50: mutex with its data
21 std::condition_variable cv_;
22 std::queue<int> queue_;
23};
Multiple Mutexes
cpp
1// CP.21: std::scoped_lock for multiple mutexes (deadlock-free)
2void transfer(Account& from, Account& to, double amount) {
3 std::scoped_lock lock(from.mutex_, to.mutex_);
4 from.balance_ -= amount;
5 to.balance_ += amount;
6}
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
1#include <concepts>
2
3// T.10 + T.11: Constrain templates with standard concepts
4template<std::integral T>
5T gcd(T a, T b) {
6 while (b != 0) {
7 a = std::exchange(b, a % b);
8 }
9 return a;
10}
11
12// T.13: Shorthand concept syntax
13void sort(std::ranges::random_access_range auto& range) {
14 std::ranges::sort(range);
15}
16
17// Custom concept for domain-specific constraints
18template<typename T>
19concept Serializable = requires(const T& t) {
20 { t.serialize() } -> std::convertible_to<std::string>;
21};
22
23template<Serializable T>
24void 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
1// SL.con.1 + SL.con.2: Prefer vector/array over C arrays
2const std::array<int, 4> fixed_data{1, 2, 3, 4};
3std::vector<std::string> dynamic_data;
4
5// SL.str.1 + SL.str.2: string owns, string_view observes
6std::string build_greeting(std::string_view name) {
7 return "Hello, " + std::string(name) + "!";
8}
9
10// SL.io.50: Use '\n' not endl
11std::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
1// Enum.3 + Enum.5: Scoped enum, no ALL_CAPS
2enum class Color { red, green, blue };
3enum class LogLevel { debug, info, warning, error };
4
5// BAD: plain enum leaks names, ALL_CAPS clashes with macros
6enum { RED, GREEN, BLUE }; // Enum.3 + Enum.5 + Enum.6 violation
7#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
1// SF.8: Include guard (or #pragma once)
2#ifndef PROJECT_MODULE_WIDGET_H
3#define PROJECT_MODULE_WIDGET_H
4
5// SF.11: Self-contained -- include everything this header needs
6#include <string>
7#include <vector>
8
9namespace project::module {
10
11class Widget {
12public:
13 explicit Widget(std::string name);
14 const std::string& name() const;
15
16private:
17 std::string name_;
18};
19
20} // namespace project::module
21
22#endif // PROJECT_MODULE_WIDGET_H
Naming Conventions
cpp
1// NL.8 + NL.10: Consistent underscore_style
2namespace my_project {
3
4constexpr int max_buffer_size = 4096; // NL.9: not ALL_CAPS (it's not a macro)
5
6class tcp_connection { // underscore_style class
7public:
8 void send_message(std::string_view msg);
9 bool is_connected() const;
10
11private:
12 std::string host_; // trailing underscore for members
13 int port_;
14};
15
16} // 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
1// Per.11: Compile-time computation where possible
2constexpr auto lookup_table = [] {
3 std::array<int, 256> table{};
4 for (int i = 0; i < 256; ++i) {
5 table[i] = i * i;
6 }
7 return table;
8}();
9
10// Per.19: Prefer contiguous data for cache-friendliness
11std::vector<Point> points; // GOOD: contiguous
12std::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: