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:
#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:
#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:
# 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();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?
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
- Segui le C++ Core Guidelines — RAII per le risorse, Rule of 5, tipizzazione forte, API pubbliche minimali
- Rendi tutto
constper 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?