Was macht C++-Programmierstandards (C++ Core Guidelines)?
Umfassende Programmierstandards für modernes C++ (C++17/20/23) abgeleitet von den C++ Core Guidelines. Setzt Typsicherheit, Ressourcensicherheit, Unveränderlichkeit und Klarheit durch.
Wann verwenden
- Schreiben von neuem C++-Code (Klassen, Funktionen, Templates)
- Überprüfen oder Refaktorieren von vorhandenem C++-Code
- Treffen von Architekturentscheidungen in C++-Projekten
- Durchsetzen eines konsistenten Stils über eine C++-Codebasis hinweg
- Wahl zwischen Sprachfeatures (z. B.
enum vs enum class, Raw Pointer vs Smart Pointer)
Wann NICHT verwenden
- Nicht-C++-Projekte
- Legacy-C-Codebasen, die keine modernen C++-Features übernehmen können
- Eingebettete/Bare-Metal-Kontexte, in denen spezifische Richtlinien mit Hardware-Beschränkungen kollidieren (selektiv anpassen)
Querschnittsprinzipien
Diese Themen ziehen sich durch die gesamten Richtlinien und bilden die Grundlage:
- RAII überall (P.8, R.1, E.6, CP.20): Ressourcenlebensdauer an Objektlebensdauer binden
- Unveränderlichkeit als Standard (P.10, Con.1-5, ES.25): Beginnen Sie mit
const/constexpr; Veränderlichkeit ist die Ausnahme
- Typsicherheit (P.4, I.4, ES.46-49, Enum.3): Verwenden Sie das Typsystem, um Fehler zur Kompilierzeit zu verhindern
- Absicht ausdrücken (P.3, F.1, NL.1-2, T.10): Namen, Typen und Konzepte sollten Zweck kommunizieren
- Komplexität minimieren (F.2-3, ES.5, Per.4-5): Einfacher Code ist korrekter Code
- Wertsemantik vor Zeigersemantik (C.10, R.3-5, F.20, CP.31): Bevorzugen Sie Rückgabe nach Wert und Objekte im Gültigkeitsbereich
Philosophie & Schnittstellen (P., I.)
Wichtige Regeln
| Regel | Zusammenfassung |
|---|
| P.1 | Ideen direkt im Code ausdrücken |
| P.3 | Absicht ausdrücken |
| P.4 | Idealerweise sollte ein Programm statisch typischer sein |
| P.5 | Kompilierzeit-Prüfung vor Laufzeit-Prüfung bevorzugen |
| P.8 | Keine Ressourcen durchsickern lassen |
| P.10 | Unveränderliche Daten veränderlichen vorziehen |
| I.1 | Schnittstellen explizit machen |
| I.2 | Nicht-konstante globale Variablen vermeiden |
| I.4 | Schnittstellen präzise und stark typisiert machen |
| I.11 | Besitz niemals über einen Raw Pointer oder eine Referenz übergeben |
| I.23 | Anzahl der Funktionsargumente gering halten |
RICHTIG
struct Temperature {
double kelvin;
};
Temperature boil(const Temperature& water);
FALSCH
double boil(double* temp);
int g_counter = 0;
Funktionen (F.*)
Wichtige Regeln
| Regel | Zusammenfassung |
|---|
| F.1 | Sinnvolle Operationen als sorgfältig benannte Funktionen kapseln |
| F.2 | Eine Funktion sollte eine einzige logische Operation ausführen |
| F.3 | Funktionen kurz und einfach halten |
| F.4 | Wenn eine Funktion zur Kompilierzeit ausgewertet werden könnte, deklarieren Sie sie constexpr |
| F.6 | Wenn Ihre Funktion nicht werfen darf, deklarieren Sie sie noexcept |
| F.8 | Reine Funktionen bevorzugen |
| F.16 | Für "Eingabe"-Parameter: günstig kopierbare Typen als Wert übergeben, andere als const& |
| F.20 | Für "Ausgabe"-Werte: Rückgabewerte Ausgabeparametern vorziehen |
| F.21 | Um mehrere "Ausgabe"-Werte zurückzugeben, ein Struktur zurückgeben bevorzugen |
| F.43 | Niemals einen Zeiger oder eine Referenz auf ein lokales Objekt zurückgeben |
Parameterübergabe
void print(int x);
void analyze(const std::string& data);
void transform(std::string s);
struct ParseResult {
std::string token;
int position;
};
ParseResult parse(std::string_view input);
void parse(std::string_view input,
std::string& token, int& pos);
Reine Funktionen und constexpr
constexpr int factorial(int n) noexcept {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
static_assert(factorial(5) == 120);
Anti-Patterns
- Rückgabe von
T&& von Funktionen (F.45)
- Verwendung von
va_arg / C-artigen Variadics (F.55)
- Erfassen per Referenz in Lambdas, die an andere Threads übergeben werden (F.53)
- Rückgabe von
const T, das Move-Semantik unterbindet (F.49)
Klassen & Klassenhierarchien (C.*)
Wichtige Regeln
| Regel | Zusammenfassung |
|---|
| C.2 | class verwenden, wenn eine Invariante existiert; struct, wenn Datenelemente unabhängig variieren |
| C.9 | Offenlegung von Elementen minimieren |
| C.20 | Wenn Sie die Definition von Standardoperationen vermeiden können, tun Sie es (Null-Regel) |
| C.21 | Wenn Sie irgendeine Kopier-/Verschiebe-/Destruktor-Operation definieren oder =delete, behandeln Sie alle (Fünf-Regel) |
| C.35 | Basisklassendestruktor: öffentlich virtuell oder geschützt nicht-virtuell |
| C.41 | Ein Konstruktor sollte ein vollständig initialisiertes Objekt erzeugen |
| C.46 | Einparametrige Konstruktoren als explicit deklarieren |
| C.67 | Eine polymorphe Klasse sollte öffentliches Kopieren/Verschieben unterdrücken |
| C.128 | Virtuelle Funktionen: genau eines von virtual, override oder final angeben |
Null-Regel
struct Employee {
std::string name;
std::string department;
int id;
};
Fünf-Regel
class Buffer {
public:
explicit Buffer(std::size_t size)
: data_(std::make_unique<char[]>(size)), size_(size) {}
~Buffer() = default;
Buffer(const Buffer& other)
: data_(std::make_unique<char[]>(other.size_)), size_(other.size_) {
std::copy_n(other.data_.get(), size_, data_.get());
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
auto new_data = std::make_unique<char[]>(other.size_);
std::copy_n(other.data_.get(), other.size_, new_data.get());
data_ = std::move(new_data);
size_ = other.size_;
}
return *this;
}
Buffer(Buffer&&) noexcept = default;
Buffer& operator=(Buffer&&) noexcept = default;
private:
std::unique_ptr<char[]> data_;
std::size_t size_;
};
Klassenhierarchie
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Circle : public Shape {
public:
explicit Circle(double r) : radius_(r) {}
double area() const override { return 3.14159 * radius_ * radius_; }
private:
double radius_;
};
Anti-Patterns
- Aufrufen virtueller Funktionen in Konstruktoren/Destruktoren (C.82)
- Verwendung von
memset/memcpy auf nicht-triviale Typen (C.90)
- Bereitstellen unterschiedlicher Standardargumente für virtuelle Funktion und Überschreibung (C.140)
- Datenelemente als
const oder Referenzen machen, was Verschieben/Kopieren unterbindet (C.12)
Ressourcenverwaltung (R.*)
Wichtige Regeln
| Regel | Zusammenfassung |
|---|
| R.1 | Ressourcen automatisch mit RAII verwalten |
| R.3 | Ein Raw Pointer (T*) ist nicht besitzend |
| R.5 | Objekte im Gültigkeitsbereich bevorzugen; nicht unnötig auf dem Heap allozieren |
| R.10 | malloc()/free() vermeiden |
| R.11 | Explizites new und delete vermeiden |
| R.20 | unique_ptr oder shared_ptr verwenden, um Besitz darzustellen |
| R.21 | unique_ptr gegenüber shared_ptr bevorzugen, es sei denn, Besitz wird geteilt |
| R.22 | make_shared() verwenden, um shared_ptrs zu erstellen |
Smart Pointer Verwendung
auto widget = std::make_unique<Widget>("config");
auto cache = std::make_shared<Cache>(1024);
void render(const Widget* w) {
if (w) w->draw();
}
render(widget.get());
RAII-Muster
class FileHandle {
public:
explicit FileHandle(const std::string& path)
: handle_(std::fopen(path.c_str(), "r")) {
if (!handle_) throw std::runtime_error("Fehler beim Öffnen: " + path);
}
~FileHandle() {
if (handle_) std::fclose(handle_);
}
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
FileHandle(FileHandle&& other) noexcept
: handle_(std::exchange(other.handle_, nullptr)) {}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (handle_) std::fclose(handle_);
handle_ = std::exchange(other.handle_, nullptr);
}
return *this;
}
private:
std::FILE* handle_;
};
Anti-Patterns
- Nacktes
new/delete (R.11)
malloc()/free() in C++-Code (R.10)
- Mehrere Ressourcenzuweisungen in einem einzelnen Ausdruck (R.13 -- Ausnahmesicherheitsgefahr)
shared_ptr, wo unique_ptr ausreicht (R.21)
Ausdrücke & Anweisungen (ES.*)
Wichtige Regeln
| Regel | Zusammenfassung |
|---|
| ES.5 | Gültigkeitsbereiche klein halten |
| ES.20 | Ein Objekt immer initialisieren |
| ES.23 | {}-Initialisierungssyntax bevorzugen |
| ES.25 | Objekte als const oder constexpr deklarieren, es sei denn, Änderung ist beabsichtigt |
| ES.28 | Lambdas für komplexe Initialisierung von const-Variablen verwenden |
| ES.45 | Magische Konstanten vermeiden; symbolische Konstanten verwenden |
| ES.46 | Verengende/verlustbehaftete arithmetische Konvertierungen vermeiden |
| ES.47 | nullptr anstelle von 0 oder NULL verwenden |
| ES.48 | Casts vermeiden |
| ES.50 | const nicht wegcasten |
Initialisierung
const int max_retries{3};
const std::string name{"widget"};
const std::vector<int> primes{2, 3, 5, 7, 11};
const auto config = [&] {
Config c;
c.timeout = std::chrono::seconds{30};
c.retries = max_retries;
c.verbose = debug_mode;
return c;
}();
Anti-Patterns
- Nicht initialisierte Variablen (ES.20)
- Verwendung von
0 oder NULL als Zeiger (ES.47 -- nullptr verwenden)
- C-artige Casts (ES.48 --
static_cast, const_cast usw. verwenden)
- Wegcasten von
const (ES.50)
- Magische Zahlen ohne benannte Konstanten (ES.45)
- Mischen von signed und unsigned Arithmetik (ES.100)
- Wiederverwendung von Namen in verschachtelten Gültigkeitsbereichen (ES.12)
Fehlerbehandlung (E.*)
Wichtige Regeln
| Regel | Zusammenfassung |
|---|
| E.1 | Frühzeitig eine Fehlerbehandlungsstrategie entwickeln |
| E.2 | Ausnahme werfen, um zu signalisieren, dass eine Funktion ihre zugewiesene Aufgabe nicht erfüllen kann |
| E.6 | RAII verwenden, um Lecks zu verhindern |
| E.12 | noexcept verwenden, wenn Werfen unmöglich oder inakzeptabel ist |
| E.14 | Zweckentworfene benutzerdefinierte Typen als Ausnahmen verwenden |
| E.15 | Als Wert werfen, per Referenz fangen |
| E.16 | Destruktoren, Freigabe und swap dürfen niemals fehlschlagen |
| E.17 | Nicht versuchen, jede Ausnahme in jeder Funktion zu fangen |
Ausnahmehierarchie
class AppError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
class NetworkError : public AppError {
public:
NetworkError(const std::string& msg, int code)
: AppError(msg), status_code(code) {}
int status_code;
};
void fetch_data(const std::string& url) {
throw NetworkError("Verbindung abgelehnt", 503);
}
void run() {
try {
fetch_data("https://api.example.com");
} catch (const NetworkError& e) {
log_error(e.what(), e.status_code);
} catch (const AppError& e) {
log_error(e.what());
}
}
Anti-Patterns
- Werfen von eingebauten Typen wie
int oder Zeichenkettenliteralen (E.14)
- Fangen als Wert (Slicing-Risiko) (E.15)
- Leere catch-Blöcke, die Fehler stillschweigend verschlucken
- Verwendung von Ausnahmen für die Kontrollflusssteuerung (E.3)
- Fehlerbehandlung basierend auf globalem Zustand wie
errno (E.28)
Konstanten & Unveränderlichkeit (Con.*)
Alle Regeln
| Regel | Zusammenfassung |
|---|
| Con.1 | Standardmäßig Objekte unveränderlich machen |
| Con.2 | Standardmäßig Memberfunktionen const machen |
| Con.3 | Standardmäßig Zeiger und Referenzen auf const übergeben |
| Con.4 | const für Werte verwenden, die sich nach der Konstruktion nicht ändern |
| Con.5 | constexpr für Werte verwenden, die zur Kompilierzeit berechenbar sind |
class Sensor {
public:
explicit Sensor(std::string id) : id_(std::move(id)) {}
const std::string& id() const { return id_; }
double last_reading() const { return reading_; }
void record(double value) { reading_ = value; }
private:
const std::string id_;
double reading_{0.0};
};
void display(const Sensor& s) {
std::cout << s.id() << ": " << s.last_reading() << '\n';
}
constexpr double PI = 3.14159265358979;
constexpr int MAX_SENSORS = 256;
Nebenläufigkeit & Parallelität (CP.*)
Wichtige Regeln
| Regel | Zusammenfassung |
|---|
| CP.2 | Datenrennen vermeiden |
| CP.3 | Explizites Teilen von beschreibbaren Daten minimieren |
| CP.4 | In Aufgaben denken, nicht in Threads |
| CP.8 | volatile nicht für Synchronisation verwenden |
| CP.20 | RAII verwenden, niemals einfaches lock()/unlock() |
| CP.21 | std::scoped_lock verwenden, um mehrere Mutexe zu erwerben |
| CP.22 | Niemals unbekannten Code aufrufen, während ein Lock gehalten wird |
| CP.42 | Nicht ohne Bedingung warten |
| CP.44 | Denken Sie daran, Ihre lock_guards und unique_locks zu benennen |
| CP.100 | Lock-freie Programmierung nur verwenden, wenn es absolut notwendig ist |
Sicheres Locking
class ThreadSafeQueue {
public:
void push(int value) {
std::scoped_lock lock(mutex_);
queue_.push(value);
}
bool pop(int& value) {
std::unique_lock lock(mutex_);
if (queue_.empty()) {
return false;
}
value = queue_.front();
queue_.pop();
return true;
}
private:
std::mutex mutex_;
std::queue<int> queue_;
};
Anti-Patterns
- Manuelles
lock()/unlock() (CP.20)
- Halten eines Locks über einen Funktionsaufruf hinweg, der Locken erwerben könnte (CP.22)
- Verwendung von
volatile als Synchronisationsmechanismus (CP.8)
- Unbedingtes Warten (CP.42)
- Lock-freie Programmierung für komplexe gemeinsam genutzte Datenstrukturen (CP.100)
Quellen