Vai al contenuto

C++ Core Guidelines e Pattern Professionali

Impara le C++ Core Guidelines, le convenzioni di denominazione, la const correctness, i principi di progettazione delle API, le pratiche di documentazione, il profiling delle prestazioni e le tecniche di debug in produzione.

Scrivere Codice C++ Professionale

Scrivere codice C++ corretto ed efficiente è necessario, ma non basta per lo sviluppo software professionale. Il C++ professionale significa anche codice leggibile (comprensibile dagli altri e dal te stesso del futuro), manutenibile (facile da modificare senza introdurre bug) e robusto (che gestisce gli errori con eleganza e fallisce in modo prevedibile). Le C++ Core Guidelines, curate da Bjarne Stroustrup e Herb Sutter, codificano decenni di esperienza sofferta in regole pratiche. Questa lezione copre le linee guida e le pratiche professionali più incisive.

Panoramica delle C++ Core Guidelines

Le Core Guidelines coprono la gestione delle risorse, le interfacce, la gestione degli errori e la denominazione. Ecco alcune tra le regole più importanti nella pratica:

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;
}

La Const Correctness come Disciplina

La const correctness non serve solo a impedire modifiche accidentali — documenta le intenzioni, abilita le ottimizzazioni del compilatore e rende il codice thread-safe per default. Rendi tutto const a meno che non debba essere modificabile:

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;
}

Principi di Progettazione delle API

La regola d'oro della progettazione delle API in C++, attribuita a Scott Meyers: Rendi le interfacce facili da usare correttamente e difficili da usare in modo sbagliato.

Usa tipi forti: invece di void set_timeout(int ms), usa void set_timeout(std::chrono::milliseconds timeout). Il chiamante non può passare per sbaglio dei secondi quando tu ti aspetti millisecondi.

Usa [[nodiscard]]: qualsiasi funzione che restituisce un codice di errore, una risorsa o un valore calcolato dovrebbe essere [[nodiscard]]. Ignorare il valore di ritorno è quasi sempre un bug.

Preferisci la semantica di valore: passa i tipi piccoli per valore e quelli più grandi per const&. Restituisci per valore (il compilatore applica la copy elision). Evita i parametri di output.

Riduci al minimo la superficie pubblica: ogni membro pubblico è un impegno che dovrai mantenere. Usa private per default ed esponi solo ciò che serve davvero.

Segui il principio della minima sorpresa: le funzioni devono fare ciò che il loro nome suggerisce, niente di più. Gli effetti collaterali devono essere evidenti dal nome (save_to_file, non process).

Convenzioni di Denominazione e Documentazione

Una denominazione coerente rende il codice leggibile a tutto il team. Convenzioni C++ diffuse:

Tipi: PascalCase — class HttpClient, struct Point3D, enum class Color
Funzioni/Metodi: snake_case o camelCase — calculate_total(), getSize()
Variabili: snake_case — int item_count, double total_price
Variabili membro: underscore finale — int count_, std::string name_
Costanti: prefisso k oppure UPPER_SNAKE — constexpr int kMaxRetries = 3
Namespace: lowercase — namespace network, namespace detail

Scegli una convenzione e applicala a tutto il progetto (clang-format + clang-tidy possono automatizzare il lavoro).

Documentazione: usa commenti in stile Doxygen per le API pubbliche:

/// @brief Si connette al database.
/// @param connection_string L'URI di connessione al database.
/// @return true se la connessione è riuscita.
/// @throws DatabaseError se la connessione fallisce.
[[nodiscard]] bool connect(std::string_view connection_string);

Profiling delle Prestazioni

Non ottimizzare mai senza aver prima fatto il profiling. Questi strumenti ti dicono dove viene davvero speso il tempo, sostituendo le supposizioni con i dati:

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();
Buona pratica

Quando rivedi del codice C++, controlla questi problemi ricorrenti:

1. Gestione delle risorse: ogni risorsa (memoria, file, lock) è gestita tramite RAII? Ci sono new/delete grezzi?
2. Const correctness: parametri, funzioni membro e variabili locali sono const dove possibile?
3. Gestione degli errori: i valori di ritorno vengono controllati? Le eccezioni sono catturate ai livelli giusti? std::expected o i codici di errore vengono usati in modo coerente?
4. Thread safety: le risorse condivise sono protette? Sono possibili data race? Un sanitizer segnalerebbe qualcosa?
5. Move semantics: gli oggetti vengono spostati anziché copiati dove ha senso? Ci sono chiamate a std::move su oggetti const (che copiano silenziosamente)?
6. Chiarezza dell'API: i nomi delle funzioni sono descrittivi? I parametri hanno tipi forti? [[nodiscard]] è usato sui valori di ritorno importanti?
7. Warning di build: il codice compila pulito con -Wall -Wextra -Wpedantic? Tutti i warning sono stati risolti, non soppressi?
8. Testing: i casi limite sono testati? I nuovi percorsi di codice sono coperti dai test? I sanitizer passano?

Insidia

Fare debug del C++ in produzione è più difficile che in fase di sviluppo. Preparati in anticipo:

Distribuisci sempre con i simboli di debug: compila i binari di produzione in RelWithDebInfo. I simboli di debug in un file .debug separato non aggiungono alcun overhead a runtime, ma rendono possibile l'analisi dei crash.

Registra dati strutturati: usa il logging strutturato (JSON) con livelli di log. Includi timestamp, ID dei thread e ID delle richieste. Usa spdlog o std::format per un logging veloce e type-safe.

Core dump: abilita i core dump (ulimit -c unlimited) e impara ad analizzarli con gdb. I symbol server conservano le informazioni di debug dei binari distribuiti.

Bug tipici in produzione:
- Memory leak — Usa ASan negli ambienti di staging, oppure strumenti come Valgrind / Heaptrack
- Deadlock — Usa TSan nei test, registra l'ordine di acquisizione dei lock, imposta timeout sui lock
- Regressioni di prestazioni — Il benchmarking continuo in CI rileva le regressioni prima del rilascio
- Compatibilità ABI — Cambiare il layout di una struct o le tabelle delle funzioni virtuali rompe l'ABI; usa l'idioma Pimpl per ABI stabili

Concetti chiave
  • Segui le C++ Core Guidelines — RAII per le risorse, Rule of 5, tipizzazione forte, API pubbliche minimali
  • Rendi tutto const per default — documenta le intenzioni, previene bug e abilita le ottimizzazioni
  • Progetta API facili da usare correttamente e difficili da usare in modo sbagliato — tipi forti, [[nodiscard]], semantica di valore
  • Fai il profiling prima di ottimizzare — usa perf, Instruments o Google Benchmark per trovare i colli di bottiglia reali
  • Nelle review controlla gestione delle risorse, const correctness, thread safety e gestione corretta degli errori
  • Preparati al debug in produzione: distribuisci con i simboli di debug, usa il logging strutturato, abilita i core dump

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Che cosa afferma la 'Rule of 5' delle C++ Core Guidelines?

2. Qual è la regola d'oro della progettazione delle API in C++?

3. Perché dovresti fare il profiling prima di ottimizzare?