Modern C++ best practices and coding standards guide
|

Modern C++ Best Practices: Write Better Code in 2026 Guide

Why Best Practices Matter

C++ gives you enormous power and flexibility. That same flexibility lets you write code that compiles cleanly but contains subtle memory errors, undefined behavior, or design problems that surface months later. Best practices are the accumulated wisdom of the C++ community — rules that prevent common mistakes and make code maintainable, safe, and efficient.

This lesson condenses the most important guidelines from the C++ Core Guidelines (maintained by Bjarne Stroustrup and Herb Sutter), decades of industry experience, and everything you’ve learned in this course into actionable rules for your daily coding.

Memory Management

// RULE 1: Never use raw new/delete in application code
// BAD:
int* data = new int[100];
// ... who deletes this? When?
delete[] data;

// GOOD:
auto data = std::make_unique<int[]>(100);
// Or better:
std::vector<int> data(100);

// RULE 2: Use smart pointers for heap allocation
auto unique = std::make_unique<Widget>();   // Single owner
auto shared = std::make_shared<Widget>();   // Shared ownership
// Raw pointer = non-owning observer only

// RULE 3: Prefer stack allocation
void good() {
    std::string name = "Alice";     // Stack — fast, automatic
    std::vector<int> data = {1,2,3}; // Stack (data on heap, managed)
    // No new, no delete, no leaks
}

// RULE 4: Follow Rule of Zero
// If all members are RAII types, compiler-generated defaults work perfectly
class GoodClass {
    std::string name_;
    std::vector<int> data_;
    std::unique_ptr<Resource> resource_;
    // No destructor, copy, or move needed!
};

Const Correctness

// RULE: Make everything const unless it needs to change

// Const variables
const int max_size = 100;
constexpr double pi = 3.14159;

// Const references (avoid copies, prevent modification)
void process(const std::string& input) {
    // input cannot be modified — communicates intent
}

// Const member functions
class Account {
    double balance_;
public:
    double balance() const { return balance_; }  // Doesn't modify state
    void deposit(double amount) { balance_ += amount; }
};

// Const iterators
void read_only(const std::vector<int>& v) {
    for (auto it = v.cbegin(); it != v.cend(); ++it) {
        // Can read but not modify
    }
}

// Const pointers
const int* ptr1;        // Pointer to const int (can't modify value)
int* const ptr2 = &x;  // Const pointer (can't change what it points to)
const int* const ptr3 = &x;  // Both

Use auto Wisely

// GOOD uses of auto:
auto it = map.find("key");    // Avoids long iterator types
auto ptr = std::make_unique<Widget>();  // Type is obvious
auto [key, value] = *it;      // Structured bindings
auto lambda = [](int x) { return x * 2; };

// BAD uses of auto — hides important type info:
auto result = compute();       // What type is result?
auto x = 0;                    // int, but confusing
auto y = f(g(h(x)));          // Type completely unclear

// RULE: Use auto when the type is:
// 1. Obvious from context (make_unique, make_shared)
// 2. Long and unimportant (iterator types)
// 3. Impossible to spell (lambdas)
// Avoid auto when the type is important for understanding

Pass by Value vs Reference

// RULES:
// 1. "In" parameters: const reference (avoids copy)
void read(const std::string& s);

// 2. Small/primitive types: by value (cheaper than indirection)
void calc(int x, double y);

// 3. "Sink" parameters (function takes ownership): by value + move
class Widget {
    std::string name_;
public:
    Widget(std::string name) : name_(std::move(name)) {}
    // Caller: Widget w("hello");          — one move
    //         Widget w(existing_string);   — one copy + one move
};

// 4. Output parameters: return by value (RVO/NRVO handles it)
std::vector<int> generate() {
    std::vector<int> result;
    // ... fill result ...
    return result;  // No copy — return value optimization
}

// 5. Never return reference to local
// const std::string& bad() {
//     std::string s = "local";
//     return s;  // DANGLING REFERENCE
// }

Error Handling

// RULE 1: Use exceptions for errors that can't be handled locally
double divide(double a, double b) {
    if (b == 0.0) throw std::invalid_argument("Division by zero");
    return a / b;
}

// RULE 2: Use std::optional for expected "no result" cases
std::optional<User> find_user(int id) {
    // Returns nullopt if not found — not an error, just absent
    return std::nullopt;
}

// RULE 3: Never throw in destructors
class Safe {
    ~Safe() noexcept {
        // Clean up, but never throw
        // If cleanup can fail, log it and move on
    }
};

// RULE 4: Make functions noexcept when they can't throw
void swap(Widget& a, Widget& b) noexcept {
    // Move operations should be noexcept
}

// RULE 5: Catch by const reference
try {
    risky();
} catch (const std::exception& e) {
    std::cerr << e.what() << "
";
}

Class Design

// RULE 1: Make interfaces hard to misuse
// BAD:
void draw(int x, int y, int width, int height, bool filled);
// draw(10, 20, 30, 40, true) — what do these numbers mean?

// GOOD:
struct Point { int x, y; };
struct Size { int width, height; };
enum class Fill { Solid, Outline };
void draw(Point position, Size size, Fill style);
// draw({10, 20}, {30, 40}, Fill::Solid) — self-documenting

// RULE 2: Use enum class, not plain enum
enum class Color { Red, Green, Blue };  // Scoped, no implicit int conversion

// RULE 3: Single Responsibility — classes do one thing
// BAD: class UserManagerDatabaseLoggerEmailSender
// GOOD: class User, class UserRepository, class Logger, class EmailService

// RULE 4: Prefer composition over inheritance
class Engine { /* ... */ };
class Car {
    Engine engine_;  // HAS-A, not IS-A
};

// RULE 5: Make data members private
class Temperature {
    double celsius_;
public:
    double celsius() const { return celsius_; }
    double fahrenheit() const { return celsius_ * 9.0/5.0 + 32.0; }
};

Use the Standard Library

// RULE: Don't reinvent the wheel

// BAD: manual loop to find element
int* found = nullptr;
for (int i = 0; i < size; ++i) {
    if (arr[i] == target) { found = &arr[i]; break; }
}

// GOOD: std::find
auto it = std::find(v.begin(), v.end(), target);

// GOOD: ranges (C++20)
auto it = std::ranges::find(v, target);

// Use algorithms: sort, find, count, transform, accumulate
// Use containers: vector, map, unordered_map, set
// Use utilities: optional, variant, string_view, span
// Use smart pointers: unique_ptr, shared_ptr

// RULE: std::vector is the default container
// Use it unless you have a measured reason not to

Naming and Style

// Consistent naming (pick a style and stick to it):
// Google style:
class MyClass {
    int member_variable_;          // Trailing underscore for members
    void DoSomething();            // PascalCase for functions
};
int local_variable;                // snake_case for locals
const int kMaxSize = 100;         // k prefix for constants

// C++ Core Guidelines / STL style:
class my_class {
    int member_variable_;
    void do_something();           // snake_case everywhere
};

// RULES:
// 1. Names should describe WHAT, not HOW
//    BAD:  int tmp, int x2, void process2()
//    GOOD: int retry_count, int max_connections, void validate_input()

// 2. One class per file (header + source)
// 3. Include guards or #pragma once in every header
// 4. Order includes: own header, project headers, library headers, std headers

Concurrency

// RULE 1: Prefer jthread over thread (C++20)
std::jthread worker([](std::stop_token st) { /* ... */ });

// RULE 2: Use lock_guard/scoped_lock, never raw lock/unlock
std::mutex mtx;
{
    std::lock_guard lock(mtx);
    // critical section
}

// RULE 3: Use std::atomic for simple shared variables
std::atomic<int> counter{0};

// RULE 4: Prefer std::async for parallel computation
auto result = std::async(std::launch::async, compute, data);

// RULE 5: Minimize shared mutable state
// The best synchronization is no synchronization

Performance

// RULE 1: Don't optimize prematurely — measure first
// Use profilers (perf, Valgrind/Callgrind, Instruments) before optimizing

// RULE 2: Move instead of copy when possible
std::vector<std::string> build_list() {
    std::vector<std::string> result;
    result.push_back(std::string("hello"));  // Move from temporary
    return result;  // RVO — no copy or move
}

// RULE 3: Reserve vector capacity when size is known
std::vector<int> v;
v.reserve(10000);  // One allocation instead of ~14 reallocations

// RULE 4: Use string_view for read-only string parameters
void process(std::string_view sv) {
    // No copy — works with string, char*, string literals
}

// RULE 5: Avoid unnecessary heap allocations
// Stack is faster than heap. Small buffer optimization in string/vector helps.

// RULE 6: Use constexpr for compile-time computation
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int f10 = factorial(10);  // Computed at compile time

Safety and Security

// RULE 1: Enable compiler warnings and treat as errors
// -Wall -Wextra -Wpedantic -Werror

// RULE 2: Use sanitizers in development
// -fsanitize=address,undefined

// RULE 3: Bounds-check containers in debug mode
// std::vector::at() instead of operator[] for checked access
v.at(i);  // Throws if i is out of range

// RULE 4: Initialize all variables
int x = 0;       // Always initialize
int* p = nullptr; // Never leave pointers uninitialized

// RULE 5: Use span instead of pointer + size (C++20)
void process(std::span<const int> data) {
    // Knows its size, can be bounds-checked
    for (int val : data) { /* ... */ }
}

// RULE 6: Avoid C-style casts
// BAD:  int x = (int)3.14;
// GOOD: int x = static_cast<int>(3.14);

C++ Core Guidelines Summary

The C++ Core Guidelines (isocpp.github.io/CppCoreGuidelines) contain hundreds of rules maintained by Bjarne Stroustrup and Herb Sutter. The most impactful ones for your daily coding are: use RAII for all resource management (R.1), prefer smart pointers over raw pointers (R.3), never transfer ownership via raw pointer (I.11), use const extensively (Con.1), make interfaces precisely typed (I.4), prefer compile-time checking to runtime checking (P.4), express ideas directly in code (P.1), and don’t leak resources (R.1). These guidelines are not arbitrary style preferences — each one prevents a category of bugs that has cost the industry billions.

Code Review Checklist

When reviewing C++ code (yours or others’), check for: no raw new/delete (use smart pointers or containers), const correctness on parameters and member functions, no uninitialized variables, proper error handling (exceptions or optional, not error codes that can be ignored), RAII for all resources, no implicit conversions or C-style casts, meaningful names, single responsibility per function and class, compiler warnings enabled and clean, and tests for public interfaces.

Practice Exercises

Exercise 1: Take a C-style C++ program (raw arrays, new/delete, manual error codes) and refactor it to use modern C++ (vectors, smart pointers, exceptions, RAII).

Exercise 2: Write a code review for a class that violates const correctness — add const to every parameter, member function, and variable that should be const.

Exercise 3: Design a type-safe API for a drawing library. Replace draw(int, int, int, int, bool, bool, int) with named types, enums, and structs that make misuse impossible at compile time.

Exercise 4: Profile a slow program using a profiler. Identify the bottleneck and optimize it — measure before and after to prove the improvement.

Best practices aren’t restrictions — they’re shortcuts to writing code that works, stays maintainable, and doesn’t surprise you with bugs six months later. Combined with everything you’ve learned — from basic syntax through RAII, concurrency, and testing — you’re equipped to write professional C++. The final lesson covers where to go from here.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *