Skip to content

C++ Core Guidelines & Professional Patterns

Learn the C++ Core Guidelines, naming conventions, const correctness, API design principles, documentation practices, performance profiling, and production debugging techniques.

Writing Professional C++ Code

Writing correct, efficient C++ is necessary but not sufficient for professional software development. Professional C++ also means code that is readable (understandable by others and your future self), maintainable (easy to modify without introducing bugs), and robust (handles errors gracefully and fails predictably). The C++ Core Guidelines, maintained by Bjarne Stroustrup and Herb Sutter, codify decades of hard-won wisdom into actionable rules. This lesson covers the most impactful guidelines and professional practices.

C++ Core Guidelines Overview

The Core Guidelines cover resource management, interfaces, error handling, and naming. Here are some of the most important rules in practice:

core_guidelines.cpp
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <span>
#include <stdexcept>

// R.1: Manage resources automatically using RAII
class DatabaseConnection {
public:
    explicit DatabaseConnection(const std::string& connection_string) {
        // Acquire resource in constructor
        std::cout << "Connected to: " << connection_string << "\n";
    }
    ~DatabaseConnection() {
        // Release resource in destructor — always runs
        std::cout << "Disconnected\n";
    }

    // Rule of 5: if you define a destructor, define or delete all 5
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;
    DatabaseConnection(DatabaseConnection&&) noexcept = default;
    DatabaseConnection& operator=(DatabaseConnection&&) noexcept = default;

    void query(std::string_view sql) const {
        std::cout << "Executing: " << sql << "\n";
    }
};

// I.4: Make interfaces precisely and strongly typed
// BAD: void process(int type, void* data, int size);
// GOOD:
enum class DataType { json, xml, csv };

void process(DataType type, std::span<const std::byte> data) {
    // type-safe, no raw pointers, size is carried with the data
}

// F.3: Keep functions short and simple
[[nodiscard]] bool is_valid_email(std::string_view email) {
    auto at_pos = email.find('@');
    if (at_pos == std::string_view::npos) return false;
    auto dot_pos = email.find('.', at_pos);
    return dot_pos != std::string_view::npos && dot_pos > at_pos + 1;
}

int main() {
    // R.5: Prefer scoped objects — no unnecessary heap allocation
    DatabaseConnection db("host=localhost dbname=mydb");
    db.query("SELECT * FROM users");
    // db is automatically closed when it goes out of scope

    // R.11: Avoid raw new/delete — use smart pointers
    auto widget = std::make_unique<std::vector<int>>(100);

    std::cout << is_valid_email("user@example.com") << "\n";   // 1
    std::cout << is_valid_email("invalid-email") << "\n";       // 0

    return 0;
}

Const Correctness as a Discipline

Const correctness is not just about preventing accidental modification — it documents intent, enables compiler optimizations, and makes code thread-safe by default. Make everything const unless it needs to be mutable:

const_correctness.cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

class StudentRegistry {
public:
    // const method — promises not to modify the object
    [[nodiscard]] size_t count() const { return students_.size(); }

    // const reference return — caller can read but not modify
    [[nodiscard]] const std::string& get_name(size_t index) const {
        if (index >= students_.size())
            throw std::out_of_range("Invalid student index");
        return students_[index];
    }

    // Non-const method — modifies the object
    void add_student(std::string name) {
        students_.push_back(std::move(name));
    }

    // const-correct algorithm usage
    [[nodiscard]] bool has_student(std::string_view name) const {
        return std::any_of(students_.cbegin(), students_.cend(),
            [name](const std::string& s) { return s == name; });
    }

private:
    std::vector<std::string> students_;
};

void print_registry(const StudentRegistry& reg) {
    // Can only call const methods on a const reference
    for (size_t i = 0; i < reg.count(); ++i) {
        std::cout << reg.get_name(i) << "\n";
    }
    // reg.add_student("Eve");  // ERROR: cannot call non-const method
}

int main() {
    StudentRegistry reg;
    reg.add_student("Alice");
    reg.add_student("Bob");

    print_registry(reg);

    // const local variables
    const auto count = reg.count();  // immutable
    const auto& name = reg.get_name(0);  // immutable reference

    std::cout << "Count: " << count << ", First: " << name << "\n";

    return 0;
}

API Design Principles

The golden rule of C++ API design, attributed to Scott Meyers: Make interfaces easy to use correctly and hard to use incorrectly.

Use strong types: Instead of void set_timeout(int ms), use void set_timeout(std::chrono::milliseconds timeout). The caller cannot accidentally pass seconds when you expect milliseconds.

Use [[nodiscard]]: Any function that returns an error code, a resource, or a computed value should be [[nodiscard]]. Ignoring the return value is almost always a bug.

Prefer value semantics: Pass small types by value, larger types by const&. Return by value (the compiler applies copy elision). Avoid output parameters.

Minimize public surface area: Every public member is a commitment you must maintain. Use private by default and only expose what's necessary.

Follow the principle of least surprise: Functions should do what their name implies, nothing more. Side effects should be obvious from the name (save_to_file, not process).

Naming Conventions & Documentation

Consistent naming makes code readable across a team. Common C++ conventions:

Types: PascalCaseclass HttpClient, struct Point3D, enum class Color
Functions/Methods: snake_case or camelCasecalculate_total(), getSize()
Variables: snake_caseint item_count, double total_price
Member variables: trailing underscore — int count_, std::string name_
Constants: k prefix or UPPER_SNAKEconstexpr int kMaxRetries = 3
Namespaces: lowercasenamespace network, namespace detail

Pick one convention and enforce it project-wide (clang-format + clang-tidy can automate this).

Documentation: Use Doxygen-style comments for public APIs:
``cpp
/// @brief Connects to the database.
/// @param connection_string The database connection URI.
/// @return true if connection succeeded.
/// @throws DatabaseError if the connection fails.
[[nodiscard]] bool connect(std::string_view connection_string);
``

Performance Profiling

Never optimize without profiling first. These tools tell you where time is actually spent, replacing guesswork with data:

benchmark.cpp
# Linux: perf — hardware performance counters
perf record -g ./my_program         # record call graph
perf report                           # interactive flamegraph viewer

# Linux: perf stat — high-level statistics
perf stat ./my_program
# Output: cycles, instructions, cache misses, branch misses, IPC

# macOS: Instruments (via Xcode)
xcrun xctrace record --template 'Time Profiler' --launch ./my_program

# Cross-platform: Google Benchmark (micro-benchmarking)
# In your code:
#include <benchmark/benchmark.h>

static void BM_VectorPushBack(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        for (int i = 0; i < state.range(0); ++i) {
            v.push_back(i);
        }
        benchmark::DoNotOptimize(v.data());
    }
}
BENCHMARK(BM_VectorPushBack)->Range(8, 1 << 16);

static void BM_VectorReserved(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        v.reserve(state.range(0));
        for (int i = 0; i < state.range(0); ++i) {
            v.push_back(i);
        }
        benchmark::DoNotOptimize(v.data());
    }
}
BENCHMARK(BM_VectorReserved)->Range(8, 1 << 16);

BENCHMARK_MAIN();
Best Practice

When reviewing C++ code, check for these common issues:

1. Resource management: Is every resource (memory, files, locks) managed by RAII? Any raw new/delete?
2. Const correctness: Are parameters, member functions, and local variables const where possible?
3. Error handling: Are return values checked? Are exceptions caught at appropriate levels? Is std::expected or error codes used consistently?
4. Thread safety: Are shared resources protected? Are data races possible? Would a sanitizer catch issues?
5. Move semantics: Are objects moved instead of copied where appropriate? Are std::move calls on const objects (which silently copy)?
6. API clarity: Are function names descriptive? Are parameters strongly typed? Is [[nodiscard]] used on important return values?
7. Build warnings: Does the code compile cleanly with -Wall -Wextra -Wpedantic? Are all warnings addressed, not suppressed?
8. Testing: Are edge cases tested? Are new code paths covered by tests? Do sanitizers pass?

Pitfall

Debugging C++ in production is harder than in development. Prepare for it:

Always ship with debug symbols: Build RelWithDebInfo for production binaries. Debug symbols in a separate .debug file add zero runtime overhead but make crash analysis possible.

Log structured data: Use structured logging (JSON) with log levels. Include timestamps, thread IDs, and request IDs. Use spdlog or std::format for fast, type-safe logging.

Core dumps: Enable core dumps (ulimit -c unlimited) and know how to analyze them with gdb. Symbol servers store debug info for deployed binaries.

Common production bugs:
- Memory leaks — Use ASan in staging environments, or tools like Valgrind / Heaptrack
- Deadlocks — Use TSan in testing, log lock acquisition order, set lock timeouts
- Performance regressions — Continuous benchmarking in CI detects regressions before deployment
- ABI compatibility — Changing struct layouts or virtual function tables breaks ABI; use the Pimpl idiom for stable ABIs

Key Takeaways
  • Follow the C++ Core Guidelines — RAII for resources, Rule of 5, strong typing, minimal public APIs
  • Make everything const by default — it documents intent, prevents bugs, and enables optimizations
  • Design APIs that are easy to use correctly and hard to use incorrectly — strong types, [[nodiscard]], value semantics
  • Profile before optimizing — use perf, Instruments, or Google Benchmark to find real bottlenecks
  • Review code for resource management, const correctness, thread safety, and proper error handling
  • Prepare for production debugging: ship with debug symbols, use structured logging, enable core dumps

Quiz — Test Your Knowledge

(15 XP)

1. What does the C++ Core Guideline 'Rule of 5' state?

2. What is the golden rule of C++ API design?

3. Why should you profile before optimizing?