Search Knowledge

© 2026 LIBREUNI PROJECT

Modern C++ Programming / Tools and Practices

Best Practices and Idioms

C++ Best Practices

Modern C++ provides powerful features that, when used correctly, lead to clean, efficient, and maintainable code. This lesson covers essential idioms and best practices.

RAII: Resource Acquisition Is Initialization

The fundamental C++ idiom: tie resource lifetime to object lifetime.

// Bad: Manual resource management
void process_file() {
    FILE* file = fopen("data.txt", "r");
    // ... process ...
    fclose(file);  // Easy to forget!
}

// Good: RAII with smart pointers
void process_file() {
    auto file = std::unique_ptr<FILE, decltype(&fclose)>(
        fopen("data.txt", "r"), fclose
    );
    // Automatically closed
}

// Better: Use RAII wrappers
class File {
    FILE* file_;
public:
    explicit File(const char* path) : file_(fopen(path, "r")) {
        if (!file_) throw std::runtime_error("Failed to open file");
    }
    ~File() { if (file_) fclose(file_); }
    
    File(const File&) = delete;
    File& operator=(const File&) = delete;
    
    FILE* get() const { return file_; }
};

Rule of Zero/Three/Five

Rule of Zero

If you don’t need custom resource management, don’t define special members:

// Good: Compiler-generated special members work perfectly
struct Person {
    std::string name;
    int age;
    std::vector<std::string> hobbies;
    // Compiler generates correct copy/move/destructor
};

Rule of Five

If you define one special member, define all:

class Resource {
    int* data_;
    
public:
    // Constructor
    Resource(int size) : data_(new int[size]) {}
    
    // Destructor
    ~Resource() { delete[] data_; }
    
    // Copy constructor
    Resource(const Resource& other) 
        : data_(new int[other.size()]) {
        std::copy(other.data_, other.data_ + size(), data_);
    }
    
    // Copy assignment
    Resource& operator=(const Resource& other) {
        if (this != &other) {
            delete[] data_;
            data_ = new int[other.size()];
            std::copy(other.data_, other.data_ + size(), data_);
        }
        return *this;
    }
    
    // Move constructor
    Resource(Resource&& other) noexcept 
        : data_(other.data_) {
        other.data_ = nullptr;
    }
    
    // Move assignment
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            other.data_ = nullptr;
        }
        return *this;
    }
};

Better: Use RAII Members

class Resource {
    std::unique_ptr<int[]> data_;
    size_t size_;
    
public:
    Resource(size_t size) 
        : data_(std::make_unique<int[]>(size)), size_(size) {}
    
    // Rule of Zero: compiler-generated special members work!
};

Const Correctness

class String {
    std::string data_;
    
public:
    // Read-only access: const method
    size_t length() const { return data_.length(); }
    const char* c_str() const { return data_.c_str(); }
    
    // Modification: non-const method
    void append(const std::string& str) { data_ += str; }
    
    // Const and non-const overloads
    char& operator[](size_t i) { return data_[i]; }
    const char& operator[](size_t i) const { return data_[i]; }
};

// Use const references to avoid copies
void print(const String& str) {  // Won't copy
    std::cout << str.c_str() << '\n';
}

Prefer References to Pointers

// Bad: Nullable, unclear ownership
void process(int* value) {
    if (value) {  // Must check for null
        *value += 10;
    }
}

// Good: Not nullable, clear value semantics
void process(int& value) {
    value += 10;  // Always valid
}

// When nullability is needed: std::optional
void process(std::optional<int>& value) {
    if (value) {
        *value += 10;
    }
}

Initialization

Prefer Uniform Initialization

// Direct initialization
int x{42};
std::string name{"Alice"};
std::vector<int> vec{1, 2, 3, 4, 5};

// Avoid narrowing conversions
int i{3.14};  // Error: narrowing

// For copy construction, use auto
auto str = std::string("Hello");  // Clear intent

Member Initialization

class Config {
    // In-class initializers (C++11)
    int timeout = 30;
    std::string host = "localhost";
    bool debug = false;
    
public:
    // Constructor: only set non-default values
    Config(std::string h) : host(std::move(h)) {}
};

Use auto Appropriately

// Good: Obvious type
auto count = 42;
auto name = std::string("Alice");
auto ptr = std::make_unique<Widget>();

// Good: Complex types
auto it = vec.begin();
auto result = some_function_with_long_return_type();

// Good: Generic code
template<typename T>
auto process(const T& value) {
    auto result = value.compute();
    return result;
}

// Bad: Type not obvious
auto x = get_value();  // What type is x?

// Better: Explicit when clarity helps
std::string x = get_value();

Range-Based For Loops

std::vector<int> vec{1, 2, 3, 4, 5};

// Read-only: const reference
for (const auto& value : vec) {
    std::cout << value << ' ';
}

// Modify: reference
for (auto& value : vec) {
    value *= 2;
}

// Copy when needed
for (auto value : vec) {
    value += 1;  // Modifies copy
}

Algorithms Over Raw Loops

// Bad: Manual loop
int sum = 0;
for (size_t i = 0; i < vec.size(); ++i) {
    sum += vec[i];
}

// Good: Algorithm expresses intent
int sum = std::accumulate(vec.begin(), vec.end(), 0);

// Better: Ranges (C++20)
int sum = std::ranges::fold_left(vec, 0, std::plus{});

More examples:

// Find element
auto it = std::find(vec.begin(), vec.end(), 42);

// Transform
std::vector<int> doubled;
std::transform(vec.begin(), vec.end(), 
               std::back_inserter(doubled),
               [](int x) { return x * 2; });

// Filter
std::vector<int> evens;
std::copy_if(vec.begin(), vec.end(),
             std::back_inserter(evens),
             [](int x) { return x % 2 == 0; });

Error Handling

Exceptions for Exceptional Cases

// Use exceptions for errors you can't handle locally
void process_file(const std::string& path) {
    std::ifstream file(path);
    if (!file) {
        throw std::runtime_error("Cannot open file: " + path);
    }
    // Process...
}

// Handle where you can recover
try {
    process_file("data.txt");
} catch (const std::exception& e) {
    std::cerr << "Error: " << e.what() << '\n';
    // Fallback action
}

Expected for Expected Errors

// Use std::expected for errors that are expected
std::expected<int, std::string> parse_int(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (...) {
        return std::unexpected("Invalid integer");
    }
}

auto result = parse_int(input);
if (result) {
    use(*result);
} else {
    handle_error(result.error());
}

Avoid Premature Optimization

// Don't do this without profiling:
void process() {
    // "Optimization": manually unrolled loop, hard to read
    for (size_t i = 0; i < n; i += 4) {
        result[i] = data[i] * 2;
        result[i+1] = data[i+1] * 2;
        result[i+2] = data[i+2] * 2;
        result[i+3] = data[i+3] * 2;
    }
}

// Instead: Write clear code, let compiler optimize
void process() {
    for (size_t i = 0; i < n; ++i) {
        result[i] = data[i] * 2;
    }
    // Modern compilers will vectorize this automatically
}

Dependency Injection

// Bad: Hard-coded dependency
class UserManager {
    Database db;  // Tightly coupled
    
public:
    void save_user(const User& user) {
        db.insert(user);
    }
};

// Good: Inject dependency
class UserManager {
    Database& db_;  // Dependency injected
    
public:
    UserManager(Database& db) : db_(db) {}
    
    void save_user(const User& user) {
        db_.insert(user);
    }
};

// Better: Use interface
class IDatabase {
public:
    virtual ~IDatabase() = default;
    virtual void insert(const User&) = 0;
};

class UserManager {
    IDatabase& db_;
    
public:
    UserManager(IDatabase& db) : db_(db) {}
    
    void save_user(const User& user) {
        db_.insert(user);
    }
};

Prefer Composition to Inheritance

// Bad: Inheritance for reuse
class Stack : public std::vector<int> {
    // Exposes vector interface (push_back, etc.)
};

// Good: Composition
class Stack {
    std::vector<int> data_;
    
public:
    void push(int value) { data_.push_back(value); }
    int pop() { 
        int val = data_.back();
        data_.pop_back();
        return val;
    }
    // Only expose stack operations
};

Value Semantics

// Prefer value semantics
std::vector<std::string> names;  // Owns strings
names.push_back("Alice");        // Makes copy/move

// Over pointer semantics
std::vector<std::string*> names;  // Doesn't own
names.push_back(new std::string("Alice"));  // Manual memory management

Type Safety with Strong Types

// Bad: Primitive obsession
void transfer(int from_account, int to_account, int amount);
transfer(123, 100, 456);  // Easy to swap arguments!

// Good: Strong types
struct AccountId {
    int value;
    explicit AccountId(int v) : value(v) {}
};

struct Amount {
    int value;
    explicit Amount(int v) : value(v) {}
};

void transfer(AccountId from, AccountId to, Amount amount);
transfer(AccountId{123}, AccountId{456}, Amount{100});  // Type-safe!

Factory Functions

// Good: make_unique/make_shared
auto ptr = std::make_unique<Widget>(args);
auto shared = std::make_shared<Widget>(args);

// Custom factory
template<typename T, typename... Args>
auto create(Args&&... args) {
    return std::make_unique<T>(std::forward<Args>(args)...);
}

auto widget = create<Widget>(arg1, arg2);

Naming Conventions

// Classes: PascalCase
class UserManager {};

// Functions/variables: snake_case
void process_data();
int user_count;

// Constants: SCREAMING_SNAKE_CASE or kPascalCase
constexpr int MAX_USERS = 100;
constexpr int kMaxUsers = 100;

// Member variables: trailing underscore
class Widget {
    int value_;
    std::string name_;
};

// Template parameters: PascalCase
template<typename ValueType, typename AllocatorType>
class Container {};

Documentation

/**
 * @brief Computes the factorial of a number.
 * 
 * @param n The number (must be non-negative)
 * @return The factorial of n
 * @throws std::invalid_argument if n < 0
 */
int factorial(int n) {
    if (n < 0) {
        throw std::invalid_argument("n must be non-negative");
    }
    // ...
}

Modern C++ Checklist

✅ Use RAII for resource management
✅ Prefer smart pointers to raw pointers
✅ Use const correctness
✅ Prefer references to pointers
✅ Initialize variables at declaration
✅ Use auto when type is obvious
✅ Prefer algorithms to raw loops
✅ Use range-based for loops
✅ Follow Rule of Zero when possible
✅ Use exceptions for exceptional cases
✅ Avoid premature optimization
✅ Prefer composition to inheritance
✅ Use strong types for type safety
✅ Document public APIs
✅ Enable compiler warnings (-Wall -Wextra)

Conceptual Check

What is the Rule of Zero?

Interactive Lab

Complete the Code

// Make this function take a non-nullable reference
void increment(int_ value) {
    value += 1;
}
Runtime Environment

Interactive Lab

1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <numeric>
5 
6int main() {
7 std::vector<int> numbers{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
8
9 // Modern C++: algorithms over raw loops
10 int sum = std::accumulate(numbers.begin(), numbers.end(), 0);
11
12 auto it = std::find_if(numbers.begin(), numbers.end(),
13 [](int x) { return x > 5; });
14
15 std::vector<int> evens;
16 std::copy_if(numbers.begin(), numbers.end(),
17 std::back_inserter(evens),
18 [](int x) { return x % 2 == 0; });
19
20 std::cout << "Sum: " << sum << '\n';
21 std::cout << "First > 5: " << *it << '\n';
22 std::cout << "Evens: ";
23 for (const auto& n : evens) {
24 std::cout << n << ' ';
25 }
26 std::cout << '\n';
27
28 return 0;
29}
System Console

Waiting for signal...