Was macht C++-Codierungsstandards (C++ Core Guidelines)?
Umfassende Codierungsstandards 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 Refactoren von vorhandenem C++-Code
- Architekturentscheidungen in C++-Projekten treffen
- Konsistenten Stil in einer C++-Codebasis durchsetzen
- Auswahl zwischen Sprachfunktionen (z. B.
enum vs enum class, Raw Pointer vs Smart Pointer)
Wann NICHT verwenden
- Nicht-C++-Projekte
- Legacy-C-Codebasen, die keine modernen C++-Funktionen übernehmen können
- Eingebettete/Bare-Metal-Kontexte, in denen bestimmte Richtlinien mit Hardware-Beschränkungen kollidieren (selektiv anpassen)
Querschnittsprinzipien
Diese Themen treten in den gesamten Richtlinien immer wieder auf und bilden die Grundlage:
- RAII überall (P.8, R.1, E.6, CP.20): Ressourcenlebensdauer an Objektlebensdauer binden
- Unveränderlichkeit standardmäßig (P.10, Con.1-5, ES.25): Mit
const/constexpr beginnen; Veränderlichkeit ist die Ausnahme
- Typsicherheit (P.4, I.4, ES.46-49, Enum.3): Das Typsystem verwenden, um Fehler zur Kompilierzeit zu verhindern
- Absicht ausdrücken (P.3, F.1, NL.1-2, T.10): Namen, Typen und Konzepte sollten den 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): Rückgabe nach Wert und Bereichsobjekte bevorzugen
Philosophie & Schnittstellen (P., I.)
Schlüsselregeln
| Regel | Zusammenfassung |
|---|
| P.1 | Ideen direkt im Code ausdrücken |
| P.3 | Absicht ausdrücken |
| P.4 | Idealerweise sollte ein Programm statisch typsicher sein |
| P.5 | Kompilierzeitprüfungen Laufzeitprüfungen vorziehen |
| P.8 | Keine Ressourcen preisgeben |
| P.10 | Unveränderliche Daten veränderlichen Daten 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 | Die 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.*)
Schlüsselregeln
| Regel | Zusammenfassung |
|---|
| F.1 | Sinnvolle Operationen als sorgfältig benannte Funktionen verpacken |
| 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 "in"-Parameter: günstig kopierbare Typen nach Wert übergeben, andere per const& |
| F.20 | Für "out"-Werte: Rückgabewerte Ausgabeparametern vorziehen |
| F.21 | Für mehrere "out"-Werte: Rückgabe eines Structs 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
T&& von Funktionen zurückgeben (F.45)
va_arg / C-artige Variadics verwenden (F.55)
- Referenzerfassung in Lambdas, die an andere Threads übergeben werden (F.53)
const T zurückgeben, was Move-Semantik verhindert (F.49)
Klassen & Klassenhierarchien (C.*)
Schlüsselregeln
| Regel | Zusammenfassung |
|---|
| C.2 | class verwenden, wenn eine Invariante existiert; struct, wenn Datenelemente unabhängig variieren |
| C.9 | Exposition von Mitgliedern minimieren |
| C.20 | Wenn Sie die Definition von Standardoperationen vermeiden können, tun Sie es (Regel des Null) |
| C.21 | Wenn Sie eine Kopier-/Verschiebe-/Destruktor-Option definieren oder =delete setzen, behandeln Sie alle (Regel der Fünf) |
| C.35 | Basisklassendestruktor: public virtual oder protected non-virtual |
| C.41 | Ein Konstruktor sollte ein vollständig initialisiertes Objekt erstellen |
| C.46 | Einargument-Konstruktoren 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 |
Regel des Null (Rule of Zero)
struct Employee {
std::string name;
std::string department;
int id;
};
Regel der Fünf (Rule of Five)
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
- Virtuelle Funktionen in Konstruktoren/Destruktoren aufrufen (C.82)
memset/memcpy auf nicht-triviale Typen anwenden (C.90)
- Unterschiedliche Standardargumente für virtuelle Funktion und Überschreiber bereitstellen (C.140)
- Datenelemente
const oder Referenzen machen, was Verschieben/Kopieren unterdrückt (C.12)
Ressourcenverwaltung (R.*)
Schlüsselregeln
| Regel | Zusammenfassung |
|---|
| R.1 | Ressourcen automatisch mit RAII verwalten |
| R.3 | Ein Raw Pointer (T*) ist nicht besitzend |
| R.5 | Bereichsobjekte bevorzugen; nicht unnötig Heap-Speicher zuweisen |
| R.10 | malloc()/free() vermeiden |
| R.11 | Expliziten Aufruf von 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, der Besitz wird geteilt |
| R.22 | make_shared() verwenden, um shared_ptrs zu erstellen |
Verwendung von Smart Pointern
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("Failed to open: " + 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 einzigen Ausdruck (R.13 -- Ausnahmesicherheitsrisiko)
shared_ptr, wo unique_ptr ausreicht (R.21)
Ausdrücke & Anweisungen (ES.*)
Schlüsselregeln
| Regel | Zusammenfassung |
|---|
| ES.5 | Geltungsbereiche klein halten |
| ES.20 | Ein Objekt immer initialisieren |
| ES.23 | {}-Initialisierungssyntax bevorzugen |
| ES.25 | Objekte const oder constexpr deklarieren, es sei denn, eine Ä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)
0 oder NULL als Zeiger verwenden (ES.47 -- nullptr verwenden)
- C-artige Casts (ES.48 --
static_cast, const_cast usw. verwenden)
const wegcasten (ES.50)
- Magische Zahlen ohne benannte Konstanten (ES.45)
- Mischen von vorzeichenbehafteter und vorzeichenloser Arithmetik (ES.100)
- Namen in verschachtelten Geltungsbereichen wiederverwenden (ES.12)
Fehlerbehandlung (E.*)
Schlüsselregeln
| Regel | Zusammenfassung |
|---|
| E.1 | Früh im Entwurf eine Fehlerbehandlungsstrategie entwickeln |
| E.2 | Eine Ausnahme werfen, um zu signalisieren, dass eine Funktion ihre zugewiesene Aufgabe nicht erfüllen kann |
| E.6 | RAII verwenden, um Leaks zu verhindern |
| E.12 | noexcept verwenden, wenn Werfen unmöglich oder inakzeptabel ist |
| E.14 | Zweckentworfene benutzerdefinierte Typen als Ausnahmen verwenden |
| E.15 | Nach Wert werfen, nach 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("connection refused", 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
- Integrierte Typen wie
int oder String-Literale werfen (E.14)
- Nach Wert fangen (Slicing-Risiko) (E.15)
- Leere catch-Blöcke, die Fehler stillschweigend verschlucken
- Ausnahmen zur Flusskontrolle verwenden (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 an const übergeben |
| Con.4 | const für Werte verwenden, die sich nach der Konstruktion nicht ändern |
| Con.5 | constexpr für zur Kompilierzeit berechenbare Werte verwenden |
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.*)
Schlüsselregeln
| Regel | Zusammenfassung |
|---|
| CP.2 | Datenwettläufe vermeiden |
| CP.3 | Explizite Freigabe von beschreibbaren Daten minimieren |
| CP.4 | In Aufgaben denken, nicht in Threads |
| CP.8 | volatile nicht zur Synchronisation verwenden |
| CP.20 | RAII verwenden, niemals einfaches lock()/unlock() |
| CP.21 | std::scoped_lock zum Erwerben mehrerer Mutexe verwenden |
| 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 Sie unbedingt müssen |
Sicheres Sperren
class ThreadSafeQueue {
public:
void push(int value) {
std::scoped_lock lock(mutex_);
queue_.push(value);
}
bool try_pop(int& value) {
std::scoped_lock lock(mutex_);
if (queue_.empty()) return false;
value = queue_.front();
queue_.pop();
return true;
}
private:
std::queue<int> queue_;
mutable std::mutex mutex_;
};
Anti-Patterns
- Nackte
lock()/unlock()-Aufrufe (CP.20)
volatile für Synchronisation (CP.8)
- Halten eines Locks während des Aufrufs von Benutzercode (CP.22)
- Bedingungsloses Warten (
std::thread::sleep anstelle von Bedingungsvariablen) (CP.42)
- Lock-freie Programmierung ohne strenge Begründung (CP.100)
Templates & Generische Programmierung (T.*)
Schlüsselregeln
| Regel | Zusammenfassung |
|---|
| T.10 | Template-Argumente für alle Templates spezifizieren (Konzepte) |
| T.20 | Vermeiden Sie "Konzepte" ohne semantische Bedeutung |
| T.41 | Nur Template-Eigenschaften in Template-Beschreibungen erfordern (keine abgeleitete) |
| T.46 | Template-Argumente in derselben Kategorie erfordern |
| T.47 | Hochgradig sichtbare Vorlagen vermeiden, die nicht verwendet werden |
Verwendung von Konzepten (C++20)
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
template<Addable T>
T sum(const std::vector<T>& values) {
T result{};
for (const auto& v : values) result = result + v;
return result;
}
Tipps zur Quelltextgestaltung (NL.*)
Schlüsselregeln
| Regel | Zusammenfassung |
|---|
| NL.1 | Nicht in Kommentaren angeben, was der Code klar sagt |
| NL.2 | Im Code beabsichtigte Informationen angeben |
| NL.10 | 0 und 1 vermeiden, außer hinter einem Kommentar |
| NL.15 | Knapp, aber nicht vage |
| NL.16 | Konventionelle Rechtschreibung verwenden |
| NL.25 | Kein Kommentar-C++ |
| NL.26 | Konventionelle Kommentarnotation verwenden (// für einzelne Zeilen, /* */ für Blöcke) |
Richtlinien für die Benennung
void process_data();
void process();
void x();
int current_time_ms;
int x;
Leistung (Per.*)
Schlüsselregeln
| Regel | Zusammenfassung |
|---|
| Per.1 | Vorzeitige Optimierung ist die Wurzel allen Übels |
| Per.2 | Was nicht gemessen wird, kann nicht optimiert werden |
| Per.3 | Komplexität nicht gegen unbelegte Effizienz eintauschen |
| Per.4 | Einfachheit kann Effizienzschranken senken, nicht erhöhen |
| Per.5 | Frühe Annahmen über den Engpass sind in der Regel falsch |
| Per.10 | Strukturierung von Daten um die Zugriffsmuster herum |
| Per.11 | Rechnen zur Kompilierzeit virtualisiert zur Laufzeit |
| Per.30 | Vermeiden Sie die Kosten für die Bereichsprüfung in kritischen Schleifen |
| Per.40 | Nach Möglichkeit verzögern: automatisch generierte Move-Operationen sind effizient |
Überprüfungsliste
Schreib-/Überprüfungszeit-TODO