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:
#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:
#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: PascalCase — class HttpClient, struct Point3D, enum class Color
Functions/Methods: snake_case or camelCase — calculate_total(), getSize()
Variables: snake_case — int item_count, double total_price
Member variables: trailing underscore — int count_, std::string name_
Constants: k prefix or UPPER_SNAKE — constexpr int kMaxRetries = 3
Namespaces: lowercase — namespace 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:
# 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();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?
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
- Follow the C++ Core Guidelines — RAII for resources, Rule of 5, strong typing, minimal public APIs
- Make everything
constby 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?