C++ Core Guidelines & professionelle Muster
Lerne die C++ Core Guidelines, Namenskonventionen, Const-Correctness, Prinzipien des API-Designs, Dokumentationspraktiken, Performance-Profiling und Debugging-Techniken für die Produktion kennen.
Professionellen C++-Code schreiben
Korrektes, effizientes C++ zu schreiben ist notwendig, für professionelle Softwareentwicklung aber nicht hinreichend. Professionelles C++ bedeutet außerdem Code, der lesbar ist (für andere und für dein künftiges Ich verständlich), wartbar (leicht zu ändern, ohne neue Bugs einzubauen) und robust (geht sauber mit Fehlern um und scheitert vorhersehbar). Die C++ Core Guidelines, gepflegt von Bjarne Stroustrup und Herb Sutter, gießen jahrzehntelang hart erarbeitetes Wissen in umsetzbare Regeln. Diese Lektion behandelt die wirkungsvollsten Richtlinien und professionellen Praktiken.
Überblick über die C++ Core Guidelines
Die Core Guidelines decken Ressourcenverwaltung, Schnittstellen, Fehlerbehandlung und Benennung ab. Hier sind einige der in der Praxis wichtigsten Regeln:
#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 als Disziplin
Bei Const-Correctness geht es nicht nur darum, versehentliche Änderungen zu verhindern — sie dokumentiert Absichten, ermöglicht Compiler-Optimierungen und macht Code standardmäßig threadsicher. Mache alles const, was nicht veränderlich sein muss:
#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;
}Prinzipien des API-Designs
Die goldene Regel des C++-API-Designs, zugeschrieben an Scott Meyers, lautet: Mache Schnittstellen leicht korrekt und schwer falsch benutzbar.
Nutze starke Typen: Statt void set_timeout(int ms) schreibe void set_timeout(std::chrono::milliseconds timeout). So kann der Aufrufer nicht versehentlich Sekunden übergeben, wo du Millisekunden erwartest.
Nutze [[nodiscard]]: Jede Funktion, die einen Fehlercode, eine Ressource oder einen berechneten Wert zurückgibt, sollte [[nodiscard]] sein. Den Rückgabewert zu ignorieren ist fast immer ein Bug.
Bevorzuge Wertsemantik: Übergib kleine Typen by value, größere per const&. Gib Werte zurück (der Compiler wendet Copy Elision an). Vermeide Ausgabeparameter.
Halte die öffentliche Oberfläche klein: Jedes öffentliche Member ist eine Zusage, die du einhalten musst. Nutze standardmäßig private und gib nur frei, was nötig ist.
Folge dem Prinzip der geringsten Überraschung: Funktionen sollten genau das tun, was ihr Name verspricht, und nicht mehr. Seiteneffekte sollten aus dem Namen ersichtlich sein (save_to_file, nicht process).
Namenskonventionen & Dokumentation
Einheitliche Benennung macht Code teamweit lesbar. Gängige C++-Konventionen:
Typen: PascalCase — class HttpClient, struct Point3D, enum class Color
Funktionen/Methoden: snake_case oder camelCase — calculate_total(), getSize()
Variablen: snake_case — int item_count, double total_price
Membervariablen: Unterstrich am Ende — int count_, std::string name_
Konstanten: Präfix k oder UPPER_SNAKE — constexpr int kMaxRetries = 3
Namensräume: lowercase — namespace network, namespace detail
Entscheide dich für eine Konvention und setze sie projektweit durch (clang-format + clang-tidy können das automatisieren).
Dokumentation: Verwende für öffentliche APIs Kommentare im Doxygen-Stil:
/// @brief Stellt eine Verbindung zur Datenbank her.
/// @param connection_string Die Verbindungs-URI der Datenbank.
/// @return true, wenn die Verbindung erfolgreich war.
/// @throws DatabaseError wenn die Verbindung fehlschlägt.
[[nodiscard]] bool connect(std::string_view connection_string);Performance-Profiling
Optimiere nie, ohne vorher zu profilen. Diese Werkzeuge sagen dir, wo die Zeit tatsächlich verbraucht wird, und ersetzen Raten durch Daten:
# 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();Achte beim Review von C++-Code auf diese häufigen Probleme:
1. Ressourcenverwaltung: Wird jede Ressource (Speicher, Dateien, Locks) per RAII verwaltet? Gibt es rohe new/delete?
2. Const-Correctness: Sind Parameter, Memberfunktionen und lokale Variablen const, wo es möglich ist?
3. Fehlerbehandlung: Werden Rückgabewerte geprüft? Werden Exceptions auf der passenden Ebene gefangen? Werden std::expected oder Fehlercodes konsistent eingesetzt?
4. Threadsicherheit: Sind gemeinsam genutzte Ressourcen geschützt? Sind Data Races möglich? Würde ein Sanitizer Probleme finden?
5. Move-Semantik: Werden Objekte verschoben statt kopiert, wo es sinnvoll ist? Gibt es std::move-Aufrufe auf const-Objekten (die stillschweigend kopieren)?
6. Klarheit der API: Sind die Funktionsnamen aussagekräftig? Sind die Parameter stark typisiert? Wird [[nodiscard]] bei wichtigen Rückgabewerten verwendet?
7. Build-Warnungen: Kompiliert der Code sauber mit -Wall -Wextra -Wpedantic? Werden alle Warnungen behoben statt unterdrückt?
8. Tests: Sind Randfälle getestet? Sind neue Codepfade von Tests abgedeckt? Laufen die Sanitizer fehlerfrei durch?
C++ in Produktion zu debuggen ist schwieriger als in der Entwicklung. Bereite dich darauf vor:
Liefere immer mit Debug-Symbolen aus: Baue Produktions-Binaries als RelWithDebInfo. Debug-Symbole in einer separaten .debug-Datei kosten zur Laufzeit nichts, machen die Crash-Analyse aber überhaupt erst möglich.
Logge strukturierte Daten: Setze auf strukturiertes Logging (JSON) mit Log-Leveln. Nimm Zeitstempel, Thread-IDs und Request-IDs mit auf. Verwende spdlog oder std::format für schnelles, typsicheres Logging.
Core Dumps: Aktiviere Core Dumps (ulimit -c unlimited) und lerne, sie mit gdb auszuwerten. Symbolserver halten die Debug-Informationen zu ausgelieferten Binaries vor.
Häufige Bugs in Produktion:
- Speicherlecks — Setze ASan in Staging-Umgebungen ein oder Werkzeuge wie Valgrind / Heaptrack
- Deadlocks — Setze TSan beim Testen ein, protokolliere die Reihenfolge der Lock-Anforderungen, setze Lock-Timeouts
- Performance-Regressionen — Kontinuierliches Benchmarking in der CI erkennt Regressionen vor dem Deployment
- ABI-Kompatibilität — Geänderte Struct-Layouts oder Virtual Function Tables brechen die ABI; nutze das Pimpl-Idiom für stabile ABIs
- Halte dich an die C++ Core Guidelines — RAII für Ressourcen, Rule of 5, starke Typisierung, minimale öffentliche APIs
- Mache standardmäßig alles
const— das dokumentiert Absichten, verhindert Bugs und ermöglicht Optimierungen - Entwirf APIs, die leicht korrekt und schwer falsch zu benutzen sind — starke Typen,
[[nodiscard]], Wertsemantik - Profile, bevor du optimierst — nutze
perf, Instruments oder Google Benchmark, um die echten Engpässe zu finden - Prüfe im Review Ressourcenverwaltung, Const-Correctness, Threadsicherheit und saubere Fehlerbehandlung
- Bereite dich auf das Debugging in Produktion vor: mit Debug-Symbolen ausliefern, strukturiert loggen, Core Dumps aktivieren
Quiz — Teste dein Wissen
(15 XP)1. Was besagt die 'Rule of 5' aus den C++ Core Guidelines?
2. Was ist die goldene Regel des C++-API-Designs?
3. Warum solltest du profilen, bevor du optimierst?