Skip to content

C++ Spickzettel

Universelle Sprache mit OOP, Generics und Low-Level-Kontrolle.

01

Erste Schritte

Hello World

Jedes C++-Programm beginnt in main(). <iostream> bietet std::cout (Standardausgabe) und std::cin (Standardeingabe). Das std::-Präfix bezieht sich auf den Standard-Namespace; using namespace std; kann es vermeiden, ist aber in Headern verpönt, weil es den globalen Namespace verschmutzt.

cpp
#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

Variablen & auto

auto leitet den Typ vom Initialisierer ab (C++11). Verwenden Sie auto, wenn der Typ offensichtlich oder verbose ist (Iteratoren). const macht einen Wert unveränderlich; constexpr wertet zur Kompilierzeit aus für echte Konstanten, die in die Binärdatei eingebettet sind.

cpp
int age = 30;
double pi = 3.14159;
char grade = 'A';
bool is_dev = true;
std::string name = "Alice";
auto x = 42;          // int
auto y = 3.14;        // double
const double TAX = 0.08;

Eingabe & Ausgabe

std::getline liest eine ganze Zeile inklusive Leerzeichen, während std::cin >> bei Leerzeichen stoppt. Das Mischen beider lässt einen Zeilenumbruch im Puffer; rufen Sie std::cin.ignore() zwischen getline und >> auf, um ihn zu verwerfen.

cpp
#include <iostream>
#include <string>

int main() {
    std::string name;
    int age;
    std::cout << "Name: ";
    std::getline(std::cin, name);
    std::cout << "Age: ";
    std::cin >> age;
    std::cout << "Hi " << name << ", " << age << "\n";
}

Referenzen

Referenzen sind Aliase, die initialisiert werden müssen und nicht auf ein anderes Objekt umgebogen werden können. Pass-by-Reference vermeidet Kopieren und erlaubt das Modifizieren der Variablen des Aufrufers. Verwenden Sie const T& für Nur-Lese-Parameter, um teure Kopien zu vermeiden.

cpp
int x = 10;
int &ref = x;   // alias for x
ref = 20;
std::cout << x;   // 20

void increment(int &n) { n++; }
int a = 5;
increment(a);   // a is now 6

Typumwandlung

Bevorzugen Sie static_cast gegenüber C-Stil-Casts, weil es explizit und zur Kompilierzeit geprüft ist, was die Absicht klar macht. std::stoi, std::stod konvertieren Strings zu Zahlen; std::to_string macht das Umgekehrte. Achten Sie auf std::out_of_range bei schlechter Eingabe.

cpp
double d = 3.99;
int i = (int)d;               // C-style: 3
int j = static_cast<int>(d);  // C++: 3

int n = 65;
char c = static_cast<char>(n);  // 'A'

// string <-> number
int num = std::stoi("42");
std::string s = std::to_string(3.14);
02

Strings

std::string-Grundlagen

std::string verwaltet seinen eigenen Speicher und wächst nach Bedarf. Anders als C-char-Arrays verwalten Sie die Länge nicht manuell. .find() gibt std::string::npos (ein riesiger Wert) zurück, wenn der Teilstring nicht gefunden wird, also vergleichen Sie immer gegen npos.

cpp
#include <string>
std::string s = "Hello";
std::cout << s.length();      // 5
std::cout << s[0];            // H
s += ", World!";
std::cout << s;               // Hello, World!
std::cout << s.substr(0, 3);  // Hel
std::cout << s.find("World"); // 7

Vergleich & Suche

Der Vergleich ist lexikografisch (Wörterbuchreihenfolge). .find() sucht vorwärts, .rfind() sucht rückwärts. Beide geben std::string::npos zurück, wenn nicht gefunden, also vergleichen Sie immer gegen npos, statt das Ergebnis als booleschen Wert zu behandeln.

cpp
std::string a = "apple", b = "banana";
if (a == b) { /* equal */ }
if (a < b) { /* apple comes before banana */ }
if (a.find("pp") != std::string::npos) {
    std::cout << "found\n";
}
size_t pos = a.rfind("p");  // last occurrence

Stringstream

stringstream verbindet Strings und typisierte Werte, nützlich zum Erstellen formatierter Strings (wie ein Puffer) oder zum Parsen leerzeichengetrennter Token. Es ist langsamer als direkte Operationen, aber sehr flexibel für Serialisierung und Deserialisierung.

cpp
#include <sstream>
// Build a string
std::ostringstream oss;
oss << "Name=" << "Alice" << "&Age=" << 30;
std::string result = oss.str();

// Parse tokens
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c;   // a=10, b=20, c=30

Raw-Strings & Mehrzeilig

Raw-String-Literale R"(...)" behandeln Backslashes und Anführungszeichen wörtlich, ideal für Regex-Patterns, Windows-Dateipfade und JSON/XML-Templates. Die Trennzeichen in den Klammern sind beliebig, z.B. R"x(...)x", um ) innerhalb zu erlauben.

cpp
std::string raw = R"(C:\Users\name\file.txt)";
// No need to escape backslashes

std::string json = R"({
  "name": "Alice",
  "age": 30
})";

char-Arrays vs. std::string

C-Stil-char-Arrays erfordern manuelle Größenverwaltung und sind fehleranfällig (Pufferüberläufe). Bevorzugen Sie std::string; verwenden Sie .c_str(), wenn Sie mit C-APIs interfacen, die const char* erwarten. Beachten Sie, dass c_str() nur gültig ist, solange der String lebt und unmodifiziert ist.

cpp
char cstr[] = "Hello";   // null-terminated, size 6
std::string cppstr = "Hello";

#include <cstring>
std::cout << strlen(cstr);      // 5
std::cout << cppstr.length();   // 5

std::string from_c = cstr;          // C -> C++
const char* to_c = cppstr.c_str();  // C++ -> C
03

Zahlen & Mathematik

Ganzzahl- & Gleitkommatypen

Verwenden Sie <cstdint>-Typen mit fester Breite (int32_t, int64_t), wenn genaue Größe plattformübergreifend wichtig ist. Das '-Zahltrennzeichen (C++14) verbessert die Lesbarkeit großer Zahlen. double ist der Standard-Gleitkommatyp und wird für Genauigkeit gegenüber float bevorzugt.

cpp
#include <cstdint>
int32_t a = 100;
int64_t big = 9'000'000'000LL;
uint8_t byte = 255;
size_t sz = sizeof(a);   // 4

float f = 1.5f;          // 4 bytes
double d = 3.14159265;   // 8 bytes (preferred)

Numerische Grenzen

<limits> bietet Type-Traits für numerische Eigenschaften. Verwenden Sie diese statt hartcodierter INT_MAX-Makros. epsilon() gibt die kleinste Unterscheidung durch Gleitkomma an, nützlich zum Vergleichen von doubles mit einer Toleranz.

cpp
#include <limits>
std::cout << std::numeric_limits<int>::max();    // 2147483647
std::cout << std::numeric_limits<int>::min();    // -2147483648
std::cout << std::numeric_limits<double>::infinity();
std::cout << std::numeric_limits<double>::epsilon();

Mathematische Funktionen

<cmath> bietet Standard-Mathematikfunktionen. Ganzzahlüberlauf ist undefiniertes Verhalten in C++; verwenden Sie int64_t oder prüfen Sie Grenzen. Für Finanzcode denken Sie daran, dass Gleitkomma unpräzise ist—ziehen Sie Integer-Cents oder eine Dezimalbibliothek in Betracht.

cpp
#include <cmath>
double x = 2.5;
std::pow(x, 3);     // 15.625
std::sqrt(x);       // 1.581
std::abs(-5);       // 5
std::floor(3.7);    // 3.0
std::ceil(3.2);     // 4.0
std::round(3.5);    // 4.0
std::fmod(10.5, 3); // 1.5

Zufallszahlen

Modernes C++ verwendet die <random>-Bibliothek statt rand(). mt19937 ist ein hochwertiger PRNG. Verteilungen (uniform_int, uniform_real, normal) bilden rohe Bits auf den gewünschten Bereich ab, ohne die Modulo-Verzerrung, die rand() % N plagt.

cpp
#include <random>
std::random_device rd;
std::mt19937 gen(rd());  // Mersenne Twister engine
std::uniform_int_distribution<int> dist(1, 100);

for (int i = 0; i < 5; i++) {
    std::cout << dist(gen) << " ";
}

Ganzzahlüberlauf & Casts

Vorzeichenbehafteter Ganzzahlüberlauf ist undefiniertes Verhalten in C++ (der Compiler kann optimieren unter der Annahme, dass es nie passiert). Casten Sie immer auf einen breiteren Typ vor dem Multiplizieren, oder prüfen Sie Grenzen. Unsigned-Überlauf wrappt modulo 2^n und ist wohldefiniert.

cpp
int a = 100'000;
int b = a * a;   // overflow! undefined behavior
long long safe = (long long)a * a;  // OK

// Check before multiplying
if (a > 0 && b > INT_MAX / a) {
    // would overflow, handle it
}
04

Kontrollfluss

If / Else

C++17 führt if mit Initialisierer ein: if (auto it = m.find(k); it != m.end()) { ... }. Dies begrenzt die Variable auf den if/else-Block, hält den umgebenden Scope sauber und vermeidet versehentliche Wiederverwendung.

cpp
int score = 85;
if (score >= 90) {
    std::cout << "A\n";
} else if (score >= 80) {
    std::cout << "B\n";
} else {
    std::cout << "C\n";
}

Switch

Verwenden Sie immer break, um unbeabsichtigtes Fall-Through zu verhindern. Das C++17 [[fallthrough]]-Attribut dokumentiert absichtliches Fall-Through, um Warnungen zu unterdrücken. Switch funktioniert mit integralen und Enum-Typen, nicht mit Strings oder Floats.

cpp
int day = 3;
switch (day) {
    case 1: std::cout << "Mon"; break;
    case 2: std::cout << "Tue"; break;
    case 3: std::cout << "Wed"; break;
    default: std::cout << "Other";
}

For-Schleifen

Range-basierte for iteriert Container sauber. Verwenden Sie const auto&, um Elemente nicht zu kopieren (wichtig für Strings und große Objekte). Um Elemente in-place zu modifizieren, verwenden Sie auto& (non-const-Referenz).

cpp
// Classic for
for (int i = 0; i < 5; i++) {
    std::cout << i << " ";
}

// Range-based for (C++11)
std::vector<int> v = {1, 2, 3};
for (int n : v) std::cout << n;
for (const auto &n : v) std::cout << n;  // no copy

While & Do-While

while prüft die Bedingung vor der Ausführung; do-while führt den Körper mindestens einmal aus, bevor geprüft wird. do-while ist nützlich für Eingabevalidierung und Menü-Schleifen, wo der Körper laufen muss, bevor die Bedingung ausgewertet werden kann.

cpp
int n = 5;
while (n > 0) {
    std::cout << n-- << " ";
}

int x;
do {
    std::cin >> x;
} while (x < 0);  // runs at least once

Break, Continue & verschachtelte Schleifen

break verlässt die nächste umschließende Schleife; continue springt zur nächsten Iteration. C++ hat kein labeled break wie Java; verwenden Sie eine Flag-Variable, oder extrahieren Sie die Schleife in eine Funktion und verwenden Sie return, um verschachtelte Schleifen zu verlassen.

cpp
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    std::cout << i << " ";  // 0 1 2 4 5 6
}

// C++ has no labeled break; use a flag
bool found = false;
for (int i = 0; i < n && !found; i++) {
    for (int j = 0; j < m; j++) {
        if (matrix[i][j] == target) { found = true; break; }
    }
}
05

Funktionen & Lambdas

Definieren & Mehrfachrückgaben

C++17 Structured Bindings (auto [a, b] = ...) entpacken Tuples, Pairs und Structs sauber. Vor C++17 verwenden Sie std::tie oder Ausgabeparameter. Rückgabe als Wert ist billig durch Move-Semantik (RVO), die die Kopie elidiert.

cpp
#include <tuple>
std::tuple<int, int> divide(int a, int b) {
    return {a / b, a % b};
}

auto [q, r] = divide(17, 5);  // q=3, r=2 (C++17)

Default & Inline

Default-Argumente lassen Aufrufer nachfolgende Parameter weglassen. inline ist ein Hinweis an den Compiler, die Funktion inline zu expandieren; moderne Compiler entscheiden Inlining selbst basierend auf Optimierungs-Flags, sodass inline hauptsächlich um ODR (One Definition Rule) geht.

cpp
inline int power(int base, int exp = 2) {
    int r = 1;
    for (int i = 0; i < exp; i++) r *= base;
    return r;
}
// power(3) == 9, power(2, 5) == 32

Funktionsüberladung

Überladung lässt Funktionen einen Namen teilen, sich aber nach Parametertypen unterscheiden. Der Compiler wählt die beste Übereinstimmung via Overload-Resolution. Mehrdeutige Überladungen verursachen Kompilierfehler; bevorzugen Sie Templates, wenn der Körper über Typen hinweg identisch ist.

cpp
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
std::string add(std::string a, std::string b) { return a + b; }

add(1, 2);          // int version
add(1.5, 2.5);      // double version
add("a", "b");      // string version

Lambda-Ausdrücke

Lambdas erstellen anonyme Funktionsobjekte inline. Die [] captured Variablen: [=] als Wert, [&] als Referenz, [x] spezifisch als Wert, [&x] spezifisch als Referenz. Sie sind essenziell für STL-Algorithmen und Callbacks. Achten Sie auf hängende Referenzen beim Capturing als Referenz.

cpp
auto square = [](int x) { return x * x; };
std::cout << square(5);  // 25

int factor = 3;
auto multiply = [factor](int x) { return x * factor; };

std::vector<int> v = {1, 2, 3};
std::for_each(v.begin(), v.end(), [](int n) {
    std::cout << n << " ";
});

Funktionszeiger & std::function

std::function (aus <functional>) hält jeden Callable: Funktionen, Lambdas, Funktoren. Es ist flexibler als rohe Funktionszeiger, hat aber einen kleinen Runtime-Overhead durch Type Erasure. Verwenden Sie es für Callbacks und das Speichern von Callables in Containern.

cpp
#include <functional>
int add(int a, int b) { return a + b; }

int (*fp)(int, int) = add;             // raw function pointer
std::function<int(int, int)> f = add;  // flexible wrapper
f = [](int a, int b) { return a * b; };

std::cout << f(3, 4);  // 12
06

STL-Container

vector

vector ist ein dynamisches Array und die Standard-Container-Wahl. push_back ist amortisiert O(1). .at() macht Grenzprüfung (wirft std::out_of_range), operator[] nicht. Rufen Sie reserve() vorab auf, wenn Sie die Größe kennen, um Reallokationen zu vermeiden.

cpp
#include <vector>
std::vector<int> v = {1, 2, 3};
v.push_back(4);
v.pop_back();
v.size();          // 3
v[0] = 0;
v.at(5);           // throws std::out_of_range
for (int n : v) std::cout << n;

map & unordered_map

map hält Schlüssel sortiert (O(log n)-Operationen); unordered_map verwendet Hashing (O(1) durchschnittlich). Verwenden Sie map, wenn Sie geordnete Iteration oder Bereichsabfragen benötigen; unordered_map für reine Lookup-Geschwindigkeit. Das Iterieren von unordered_map ist ungeordnet.

cpp
#include <map>
#include <unordered_map>
std::map<std::string, int> ordered;   // sorted by key (red-black tree)
ordered["Alice"] = 30;

std::unordered_map<std::string, int> hashed;  // hash table
hashed["Bob"] = 25;

for (auto &[k, v] : ordered) {  // C++17 structured binding
    std::cout << k << ":" << v;
}

set & unordered_set

set speichert eindeutige sortierte Elemente (O(log n)). unordered_set ist die Hash-basierte Version (O(1) durchschnittlich). Verwenden Sie sie für Deduplizierung und Mitgliedschaftstests. lower_bound/upper_bound ermöglichen Bereichsabfragen auf geordneten Sets.

cpp
#include <set>
std::set<int> s = {3, 1, 4, 1, 5};
// s contains: 1, 3, 4, 5 (sorted, unique)
s.insert(2);
s.erase(1);
if (s.count(4)) std::cout << "found";
auto it = s.lower_bound(3);  // first >= 3

array & deque

array ist ein festes, Stack-allokiertes Array mit STL-Schnittstelle (sicherer als C-Arrays, kein Zerfall zu Zeiger). deque (Double-Ended-Queue) unterstützt O(1) push/pop an beiden Enden, im Gegensatz zu vector, der an vorn O(n) ist.

cpp
#include <array>
#include <deque>
std::array<int, 3> arr = {1, 2, 3};  // fixed size, stack-allocated
arr.size();  // 3

std::deque<int> dq = {1, 2, 3};
dq.push_front(0);
dq.push_back(4);
// dq: 0, 1, 2, 3, 4

tuple & pair

tuple hält heterogene Werte beliebiger Typen. pair ist ein 2-Element-tuple. Structured Bindings (C++17) zerlegen sie in benannte Variablen. Häufig beim Iterieren von Maps, deren Elemente Paare aus (Schlüssel, Wert) sind.

cpp
#include <tuple>
std::tuple<int, std::string, double> t = {1, "Alice", 3.14};
auto [id, name, val] = t;  // C++17 structured binding

std::pair<int, int> p = {1, 2};
std::cout << p.first << p.second;

auto [a, b] = std::make_pair(10, 20);
07

Zeiger & Speicher

Rohe Zeiger

Zeiger speichern Speicheradressen. & holt die Adresse, * dereferenziert. Zeigerarithmetik funktioniert auf Arrays. Rohe Zeiger verfolgen kein Ownership, was zu Lecks und hängenden Zeigern führt—bevorzugen Sie Smart Pointer für eigene Ressourcen.

cpp
int x = 10;
int *ptr = &x;        // address of x
std::cout << *ptr;    // 10 (dereference)
*ptr = 20;
std::cout << x;       // 20

int arr[] = {1, 2, 3};
int *p = arr;
std::cout << *(p + 1);  // 2

Referenzen vs. Zeiger

Referenzen sind sicherer (nie null, immer gültig) und haben sauberere Syntax. Verwenden Sie Referenzen für Funktionsparameter und Rückgabewerte. Verwenden Sie Zeiger, wenn null ein sinnvoller Zustand ist oder wenn Sie umgebogen müssen, worauf gezeigt wird.

cpp
int x = 10;
int &ref = x;    // must init, cannot reseat
int *ptr = &x;   // can be null, can reassign

ref = 20;        // x = 20
*ptr = 30;       // x = 30

// References cannot be null, safer for parameters
void foo(const std::string &s);  // preferred
void bar(std::string *s);        // s might be null

unique_ptr

unique_ptr ist alleiniges Ownership eines Heap-Objekts. Es kann nicht kopiert, nur verschoben werden. Löscht automatisch, wenn es den Scope verlässt (RAII). Dies ist der Standard-Smart-Pointer für die meisten Anwendungsfälle—Null-Overhead über einem rohen Zeiger.

cpp
#include <memory>
auto p = std::make_unique<int>(42);
std::cout << *p;  // 42
// auto p2 = p;   // ERROR: cannot copy
auto p2 = std::move(p);  // transfer ownership
// p is now nullptr

shared_ptr & weak_ptr

shared_ptr verwendet Referenzzählung; das Objekt wird freigegeben, wenn der letzte shared_ptr zerstört wird. weak_ptr beobachtet ohne den Zähler zu beeinflussen und bricht Referenzzyklen. Vermeiden Sie Zyklen von shared_ptr (sie lecken, weil der Zähler nie null erreicht).

cpp
#include <memory>
auto a = std::make_shared<int>(42);
auto b = a;  // both point to same object
std::cout << a.use_count();  // 2

std::weak_ptr<int> w = a;  // observer, no ownership
if (auto locked = w.lock()) {
    std::cout << *locked;  // 42
}

RAII & new/delete

RAII bindet die Ressourcenlebensdauer an den Objekt-Scope: Konstruktoren akquirieren, Destruktoren geben frei. Dies garantiert Cleanup, selbst wenn Exceptions propagieren. Bevorzugen Sie Vektoren und Smart Pointer gegenüber manuellem new/delete—sie implementieren RAII für Sie.

cpp
// Manual new/delete (avoid in modern C++)
int *p = new int(42);
delete p;

// RAII: resource acquisition is initialization
class Buffer {
    int *data;
public:
    Buffer(size_t n) : data(new int[n]) {}
    ~Buffer() { delete[] data; }  // auto cleanup
};

// Modern: use containers/smart pointers instead
std::vector<int> buf(100);  // no manual delete needed
08

Klassen & OOP

Klasse & Konstruktor

Die Member-Initialisierer-Liste (: name(...), age(...)) initialisiert Member, bevor der Körper läuft, effizienter als Zuweisung im Körper. Markieren Sie Getter als const, um Aufrufe auf const-Objekten zu erlauben. std::move vermeidet das Kopieren des String-Parameters.

cpp
class Person {
    std::string name;
    int age;
public:
    Person(std::string n, int a) : name(std::move(n)), age(a) {}
    std::string getName() const { return name; }
    int getAge() const { return age; }
};

Person p("Alice", 30);
std::cout << p.getName();

Zugriffsmodifikatoren & Kapselung

private-Member sind nur innerhalb der Klasse zugänglich; protected erlaubt Unterklassen; public ist für alle offen. Kapselung verbirgt Implementierungsdetails und exponiert eine stabile Schnittstelle. Verwenden Sie standardmäßig private und exponieren Sie nur, was nötig ist.

cpp
class Account {
private:
    double balance;
protected:
    std::string owner;
public:
    Account(double b) : balance(b) {}
    double getBalance() const { return balance; }
    void deposit(double amt) { if (amt > 0) balance += amt; }
};

Vererbung & Virtual

virtual ermöglicht Runtime-Polymorphismus—der Aufruf von speak() über ein Animal* dispatcht zu Dogs Version. Deklarieren Sie immer einen virtuellen Destruktor in Basisklassen, sodass das Löschen über einen Basis-Zeiger den abgeleiteten Destruktor aufruft. override fängt Tippfehler ab.

cpp
class Animal {
public:
    virtual void speak() { std::cout << "..."; }
    virtual ~Animal() = default;  // virtual destructor!
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Woof"; }
};

Animal *a = new Dog();
a->speak();  // Woof (polymorphism)
delete a;

Abstrakte Klassen & Interfaces

Eine rein virtuelle Funktion (= 0) macht die Klasse abstrakt—Sie können sie nicht instanziieren. Klassen mit nur rein virtuellen Funktionen wirken wie Java-Interfaces. Konkrete Unterklassen müssen alle rein virtuellen Funktionen implementieren, oder sie bleiben abstrakt.

cpp
class Shape {
public:
    virtual double area() const = 0;  // pure virtual
    virtual ~Shape() = default;
};

class Circle : public Shape {
    double r;
public:
    Circle(double r) : r(r) {}
    double area() const override {
        return 3.14159 * r * r;
    }
};

Operator-Überladung

Operator-Überladung lässt benutzerdefinierte Typen mit +, <<, == usw. arbeiten. Überladen Sie nur, wenn die Bedeutung intuitiv ist (Math-Typen, Iteratoren). Der <<-Operator wird häufig via friend für Stream-Ausgabe überladen, was cout << myObject ermöglicht.

cpp
class Vec2 {
    double x, y;
public:
    Vec2(double x, double y) : x(x), y(y) {}
    Vec2 operator+(const Vec2 &o) const {
        return {x + o.x, y + o.y};
    }
    friend std::ostream &operator<<(std::ostream &os, const Vec2 &v) {
        return os << "(" << v.x << "," << v.y << ")";
    }
};

Vec2 a(1, 2), b(3, 4);
std::cout << a + b;  // (4,6)
09

Templates & Generics

Funktions-Templates

Funktions-Templates generieren typspezifische Versionen zur Kompilierzeit. Der Compiler leitet T aus Argumenten ab; Sie können es auch explizit angeben. Templates sind Null-Kosten-Abstraktionen—kein Runtime-Overhead, aber sie erhöhen Kompilierzeit und Binärgröße.

cpp
template <typename T>
T max_val(T a, T b) {
    return (a > b) ? a : b;
}

std::cout << max_val(3, 7);           // int: 7
std::cout << max_val(3.14, 2.71);     // double: 3.14
std::cout << max_val<std::string>("a", "b");  // explicit

Klassen-Templates

Klassen-Templates parametrisieren gesamte Klassen über Typen. Die Standard-Container (vector, map) sind alle Templates. Template-Code muss in Headern sein (oder verwenden Sie explizite Instanziierung), weil der Compiler die volle Definition benötigt, um Code zu generieren.

cpp
template <typename T>
class Stack {
    std::vector<T> data;
public:
    void push(T v) { data.push_back(v); }
    T pop() { T v = data.back(); data.pop_back(); return v; }
    bool empty() const { return data.empty(); }
};

Stack<int> si;
si.push(1); si.push(2);
Stack<std::string> ss;
ss.push("hi");

Template-Spezialisierung

Vollständige Spezialisierung bietet eine benutzerdefinierte Implementierung für einen spezifischen Typ. Partielle Spezialisierung (nur für Klassen-Templates) passt für eine Kategorie von Typen an (z.B. alle Zeiger-Typen). Nützlich zum Optimieren oder Spezialbehandeln von Verhalten.

cpp
template <typename T>
T identity(T x) { return x; }

// Full specialization for bool
template <>
bool identity<bool>(bool x) {
    std::cout << "bool!";
    return x;
}

identity(42);     // generic
identity(true);   // specialized

Variadische Templates

Variadische Templates akzeptieren jede Anzahl von Argumenten via Parameter Packs (...). Sie rekursieren, um jedes Argument zu verarbeiten. C++17 Fold-Ausdrücke vereinfachen dies: (std::cout << ... << args). Schwer verwendet in std::make_shared, std::tuple.

cpp
template <typename T>
void print(T v) { std::cout << v << "\n"; }

template <typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first << ", ";
    print(rest...);  // recurse
}

print(1, "hello", 3.14);  // 1, hello, 3.14

Concepts (C++20)

Concepts (C++20) beschränken Template-Parameter mit lesbaren Anforderungen und ersetzen arkane enable_if/SFINAE. Sie produzieren viel klarere Fehlermeldungen, wenn Beschränkungen nicht erfüllt sind. Verwenden Sie Standard-Concepts wie std::integral, std::floating_point, std::convertible_to.

cpp
#include <concepts>
template <std::integral T>
T add(T a, T b) { return a + b; }

template <typename T>
requires std::floating_point<T>
T half(T x) { return x / 2; }

// Abbreviated form
auto square(std::integral auto x) { return x * x; }
10

STL-Algorithmen

sort & find

STL-Algorithmen operieren auf Iterator-Bereichen [begin, end). sort ist O(n log n). find ist linear; für sortierte Bereiche verwenden Sie binary_search/lower_bound (O(log n)). Übergeben Sie benutzerdefinierte Komparatoren (Lambdas oder std::greater) für benutzerdefinierte Sortierung.

cpp
#include <algorithm>
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end());          // 1 1 3 4 5 9
std::sort(v.begin(), v.end(), std::greater<>());  // descending

auto it = std::find(v.begin(), v.end(), 4);
if (it != v.end()) std::cout << "found";

transform & for_each

transform bildet jedes Element auf einen neuen Wert ab (wie map in funktionalen Sprachen). for_each wendet eine Funktion für Seiteneffekte an. C++20 Ranges erlauben v | views::transform(...) für einen saubereren Pipeline-Stil ohne begin/end-Iteratoren.

cpp
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
               [](int x) { return x * x; });
// squared: 1 4 9 16

std::for_each(v.begin(), v.end(), [](int &x) { x *= 2; });
// v: 2 4 6 8

accumulate & count

accumulate (aus <numeric>) faltet einen Bereich mit einer Operation. Das dritte Argument ist der Initialwert und bestimmt den Ergebnistyp—verwenden Sie 0.0 für double-Summen. count gibt zurück, wie viele Elemente einem Wert entsprechen; count_if verwendet ein Prädikat.

cpp
#include <numeric>
std::vector<int> v = {1, 2, 3, 4, 5};
int sum = std::accumulate(v.begin(), v.end(), 0);  // 15
int product = std::accumulate(v.begin(), v.end(), 1,
                              std::multiplies<>());  // 120

int cnt = std::count(v.begin(), v.end(), 3);  // 1

copy, remove & unique

remove löscht nicht tatsächlich—es verschiebt nicht übereinstimmende Elemente nach vorne und gibt einen neuen End-Iterator zurück. Kombinieren Sie mit .erase() für das Erase-Remove-Idiom. unique verdichtet ähnlich aufeinanderfolgende Duplikate; sortieren Sie zuerst, um vollständig zu deduplizieren.

cpp
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> dest;
std::copy(v.begin(), v.end(), std::back_inserter(dest));

auto end = std::remove(v.begin(), v.end(), 3);  // erase-remove idiom
v.erase(end, v.end());

std::vector<int> u = {1, 1, 2, 2, 3};
u.erase(std::unique(u.begin(), u.end()), u.end());  // 1 2 3

min, max & clamp

min/max geben den kleineren/größeren von zwei Werten oder einer Initialisiererliste zurück. minmax gibt beide als Paar zurück. clamp (C++17) beschränkt einen Wert auf einen Bereich und ersetzt manuelle if/else-Grenzprüfungen—nützlich für Eingabevalidierung und UI-Koordinaten.

cpp
int a = 3, b = 7;
std::cout << std::max(a, b);   // 7
std::cout << std::min(a, b);   // 3

auto [mn, mx] = std::minmax({3, 1, 4, 1, 5});  // mn=1, mx=5

int score = 105;
int clamped = std::clamp(score, 0, 100);  // 100
11

Fehlerbehandlung

Exceptions: try/catch

Werfen Sie Exceptions als Wert, fangen Sie als const-Referenz, um Slicing zu vermeiden. Das Fangen von std::exception fängt alle Standard-Exceptions über die Basisklasse. Exceptions sind für außergewöhnliche Fälle, nicht normaler Kontrollfluss—sie haben Overhead beim Werfen.

cpp
#include <stdexcept>
try {
    int x = 10, y = 0;
    if (y == 0) throw std::runtime_error("division by zero");
    std::cout << x / y;
} catch (const std::exception &e) {
    std::cerr << "Error: " << e.what();
}

Standard-Exception-Hierarchie

<stdexcept> bietet Standard-Exception-Typen. logic_error ist für Programmiererfehler (vor Runtime erkennbar); runtime_error für unvorhersehbare Runtime-Bedingungen. Leiten Sie benutzerdefinierte Exceptions von std::runtime_error ab, sodass sie sich in Standard-Catch-Blöcke integrieren.

cpp
#include <stdexcept>
throw std::runtime_error("runtime error");
throw std::logic_error("logic error");
throw std::out_of_range("index out of range");
throw std::invalid_argument("bad argument");
throw std::bad_alloc();  // thrown by new on allocation failure

// std::exception is the base; .what() returns the message

Benutzerdefinierte Exceptions

Leiten Sie benutzerdefinierte Exceptions von einer Standard-Basis ab, sodass sie sich in catch(const std::exception&) integrieren. Fügen Sie Kontextfelder hinzu (Dateipfade, Fehlercodes), die beim Debuggen helfen. Übergeben Sie die Nachricht immer an den Basis-Konstruktor, sodass what() funktioniert.

cpp
class FileError : public std::runtime_error {
public:
    FileError(const std::string &msg, const std::string &path)
        : std::runtime_error(msg + ": " + path), path_(path) {}
    const std::string &path() const { return path_; }
private:
    std::string path_;
};

try { throw FileError("not found", "data.txt"); }
catch (const FileError &e) {
    std::cerr << e.what() << " at " << e.path();
}

noexcept & RAII-Sicherheit

noexcept verspricht, dass eine Funktion nicht wirft, und ermöglicht Compiler-Optimierungen. Wenn sie doch wirft, wird std::terminate aufgerufen. RAII garantiert, dass Destruktoren während Stack-Unwinding laufen, sodass Ressourcen freigegeben werden, selbst wenn Exceptions den Call-Stack hochpropagieren.

cpp
void safe_op() noexcept {
    // guaranteed not to throw
}

void risky() {
    std::vector<int> v(1000);  // RAII: cleanup on exception
    throw std::runtime_error("oops");
    // v's destructor still runs during stack unwinding
}

Assertions

assert() prüft Bedingungen in Debug-Builds; es wird in Release entfernt (wenn NDEBUG definiert ist), also verwenden Sie es nicht für Produktionsprüfungen. Verwenden Sie es für interne Invarianten, die Bugs anzeigen. Für benutzerseitige Validierung werfen Sie Exceptions oder geben Fehlercodes zurück.

cpp
#include <cassert>
double sqrt_safe(double x) {
    assert(x >= 0 && "sqrt of negative");
    return std::sqrt(x);
}
// In release builds (NDEBUG defined), assert is removed
12

Datei-I/O & Streams

Datei lesen

ifstream öffnet eine Datei zum Lesen. Prüfen Sie immer, ob das Öffnen erfolgreich war (!file wertet bei Fehlschlag zu true aus). getline liest zeilenweise inklusive Leerzeichen. Der Destruktor des Streams schließt die Datei automatisch (RAII), sodass kein manuelles Schließen nötig ist.

cpp
#include <fstream>
#include <string>
std::ifstream file("input.txt");
if (!file) { std::cerr << "cannot open"; return 1; }

std::string line;
while (std::getline(file, line)) {
    std::cout << line << "\n";
}

Datei schreiben

ofstream schreibt in eine Datei und schneidet standardmäßig ab. Verwenden Sie std::ios::app zum Anhängen, std::ios::binary für Binärmodus. Der <<-Operator funktioniert genau wie std::cout. Flushen Sie mit out.flush() oder verwenden Sie std::endl (was auch flusht).

cpp
#include <fstream>
std::ofstream out("output.txt");
if (!out) return 1;

out << "Line 1\n";
out << "Value: " << 42 << "\n";

out.close();  // optional, destructor closes
// Append mode: std::ofstream("f.txt", std::ios::app);

String-Streams

ostringstream baut Strings aus gemischten Typen (wie ein Puffer). istringstream parst Strings in typisierte Werte. Sie sind langsamer als direkte String-Operationen, aber sehr bequem für Serialisierung, URL-Building und Token-Parsing.

cpp
#include <sstream>
// Build a string from mixed types
std::ostringstream oss;
oss << "Name=" << "Alice" << "&Age=" << 30;
std::string url = oss.str();

// Parse tokens from a string
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c;

Binärdateien

Binärmodus vermeidet Zeilenumbruch-Übersetzung und ist kompakter als Text. write/read nehmen char* und Byte-Anzahl—verwenden Sie reinterpret_cast für Structs. Beachten Sie: Binärdateien sind nicht über Architekturen hinweg portabel (Endianness, Struct-Padding unterscheiden sich).

cpp
#include <fstream>
struct Record { int id; double value; };

// Write
std::ofstream out("data.bin", std::ios::binary);
Record r{1, 3.14};
out.write(reinterpret_cast<char*>(&r), sizeof(r));

// Read
std::ifstream in("data.bin", std::ios::binary);
Record r2;
in.read(reinterpret_cast<char*>(&r2), sizeof(r2));

Formatierte Ausgabe (C++20 fmt)

std::format (C++20) bringt Python-artige Format-Strings nach C++ und ersetzt unübersichtliche iomanip-Manipulatoren. Für älteren Code bietet <iomanip> setprecision, setw, setfill. Die {fmt}-Bibliothek ist eine beliebte Pre-C++20-Alternative mit derselben Syntax.

cpp
#include <format>  // C++20
std::string s = std::format("Hello, {}! You are {}.", "Alice", 30);
std::cout << std::format("{:.2f}", 3.14159);  // 3.14
std::cout << std::format("{:>10}", "right");   // padded

// Pre-C++20: use iomanip
#include <iomanip>
std::cout << std::fixed << std::setprecision(2) << 3.14159;
13

Smart Pointer

unique_ptr - Exklusives Ownership

unique_ptr ist der Standard-Smart-Pointer—verwenden Sie ihn, wenn ein Eigentümer genug ist. Er hat Null-Overhead gegenüber rohen Zeigern. make_unique wird bevorzugt (Exception-sicher). Kann nicht kopiert, nur verschoben werden. Benutzerdefinierte Deleter ermöglichen RAII für C-Ressourcen wie FILE* oder Sockets.

cpp
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1;  // ERROR: cannot copy
std::unique_ptr<int> p2 = std::move(p1);  // OK: transfer ownership
// p1 is now nullptr

// Custom deleter
auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> fp(fopen("f.txt", "r"), deleter);

shared_ptr - Geteiltes Ownership

shared_ptr verwendet Referenzzählung—mehrere Zeiger können dasselbe Objekt besitzen. Das Objekt wird zerstört, wenn der Refcount 0 erreicht. make_shared wird bevorzugt (einzelne Allokation für Objekt + Control Block). Schwerer als unique_ptr aufgrund von atomarem Refcount und Control Block. Verwenden Sie, wenn Ownership genuinely geteilt ist.

cpp
#include <memory>
auto p1 = std::make_shared<int>(42);
auto p2 = p1;  // OK: both share ownership
std::cout << *p1 << " " << p1.use_count();  // 42 2

// Control block holds refcount + deleter + allocator
// Refcount is atomic (thread-safe), but object access is NOT
std::shared_ptr<int> p3{new int{10}};  // uses non-array new

// Thread-safe: refcount operations are atomic
// NOT thread-safe: accessing the pointed-to object

weak_ptr - Zyklen brechen

weak_ptr ist ein nicht-besitzender Beobachter eines shared_ptr. Erhöht nicht den Refcount. Verwenden Sie lock(), um temporär einen shared_ptr zu erhalten (gibt null zurück, wenn das Objekt zerstört wurde). Essenziell zum Brechen von Referenzzyklen (z.B. doppelt verkettete Listen, Eltern-Kind-Beziehungen), die Speicherlecks verursachen würden.

cpp
#include <memory>
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // weak to avoid cycle
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;  // weak_ptr from shared_ptr

// To use a weak_ptr, lock() into a shared_ptr
if (auto locked = b->prev.lock()) {
    std::cout << "prev exists";
} else {
    std::cout << "prev expired";
}

Smart Pointer mit Arrays

Smart Pointer können Arrays verwalten. unique_ptr<T[]> bietet operator[] und korrektes delete[]. shared_ptr<T[]> wird seit C++17 unterstützt. Allerdings sind std::vector oder std::array fast immer besser—sicherer, ergonomischer und selbstdokumentierend. Verwenden Sie Smart-Array-Pointer nur beim Interfacen mit Legacy-APIs.

cpp
#include <memory>
// C++17: shared_ptr supports arrays
std::shared_ptr<int[]> arr1(new int[10]);
arr1[0] = 42;

// unique_ptr with arrays (partial specialization exists)
std::unique_ptr<int[]> arr2 = std::make_unique<int[]>(10);
arr2[5] = 100;

// Prefer std::array or std::vector over raw arrays
#include <vector>
std::vector<int> v(10);  // better choice

enable_shared_from_this

Wenn ein Objekt einen shared_ptr zu sich selbst zurückgeben muss, bietet enable_shared_from_this das sichere shared_from_this(). Das direkte Aufrufen von shared_ptr<T>(this) würde einen zweiten Control Block erstellen und zu Double-Free führen. Das Objekt muss bereits von einem shared_ptr verwaltet werden, oder shared_from_this() wirft bad_weak_ptr.

cpp
#include <memory>
class Widget : public std::enable_shared_from_this<Widget> {
public:
    std::shared_ptr<Widget> getPtr() {
        return shared_from_this();  // safe
        // return shared_ptr<Widget>(this);  // BAD: double delete
    }
};

auto w = std::make_shared<Widget>();
auto w2 = w->getPtr();  // shares ownership, refcount = 2
14

Move-Semantik & Rvalue-Referenzen

Lvalues, Rvalues und Referenzen

Lvalues haben Identität und persistieren über einen einzelnen Ausdruck hinaus (benannte Objekte). Rvalues sind temporäre oder literale Werte. T& bindet an Lvalues, T&& bindet an Rvalues. const T& ist besonders—es bindet an beide. Das Verstehen dieser Unterscheidung ist die Grundlage der Move-Semantik.

cpp
int x = 10;          // x is an lvalue
int& lref = x;       // lvalue reference
int&& rref = 20;     // rvalue reference (binds to temporary)
// int& bad = 20;    // ERROR: can't bind lref to rvalue
const int& cref = 20;  // OK: const lref binds to rvalue

int y = x + 5;       // (x + 5) is an rvalue (prvalue)
std::string s1 = "hi";
std::string&& rr = std::move(s1);  // rr is named rvalue

std::move und Move-Konstruktoren

std::move bewegt nichts—es castet zu Rvalue, wodurch Move-Konstruktor/-Zuweisung ausgewählt wird. Move-Operationen sollten noexcept sein, damit Container sie während Reallokation verwenden können (sonst fallen sie für Exception-Sicherheit auf Kopie zurück). Nach einem Move befindet sich das Quellobjekt in einem gültigen, aber unspezifizierten Zustand.

cpp
class Buffer {
    int* data;
    size_t size;
public:
    // Move constructor: steal resources
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }
    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data; size = other.size;
            other.data = nullptr; other.size = 0;
        }
        return *this;
    }
};

Buffer a(100);
Buffer b = std::move(a);  // calls move ctor, a is now empty

Perfect Forwarding

Perfect Forwarding übergibt Argumente an eine andere Funktion, während die Wertkategorie (Lvalue vs. Rvalue) erhalten bleibt. T&& in einem deduzierten Kontext ist eine 'Forwarding-Referenz' (keine Rvalue-Referenz). std::forward<T> castet bedingt: Lvalue wenn T T& ist, Rvalue wenn T T&& ist. Essenziell für Factory-Funktionen und Wrapper.

cpp
#include <utility>
template <typename T, typename Arg>
auto make_unique(Arg&& arg) {
    return std::unique_ptr<T>(new T(std::forward<Arg>(arg)));
}

template <typename... Args>
void log(Args&&... args) {
    // std::forward preserves value category
    log_impl(std::forward<Args>(args)...);
}

std::string s = "hello";
log(s);              // Arg = std::string&  (lvalue)
log(std::string());  // Arg = std::string&& (rvalue)

Rule of Five / Rule of Zero

Rule of Five: Wenn eine Klasse eine Ressource verwaltet, müssen Sie Destruktor, Copy-Ctor, Copy-Assign, Move-Ctor, Move-Assign definieren. Rule of Zero: Bevorzugen Sie das Komponieren von RAII-Typen (vector, string, Smart Pointer), sodass die compiler-generierten speziellen Member korrekt sind. Dies eliminiert fehleranfällige manuelle Ressourcenverwaltung.

cpp
// Rule of Five: if you define any of these, define all 5
class Resource {
    int* data;
public:
    Resource(size_t n) : data(new int[n]) {}
    ~Resource() { delete[] data; }
    Resource(const Resource& o);             // copy ctor
    Resource& operator=(const Resource& o);  // copy assign
    Resource(Resource&& o) noexcept;         // move ctor
    Resource& operator=(Resource&& o) noexcept;  // move assign
};

// Rule of Zero: prefer to use RAII types
class Better {
    std::vector<int> data;  // handles everything
public:
    Better() = default;  // compiler-generated funcs are correct
};

Return Value Optimization (RVO/NRVO)

RVO/NRVO erlaubt dem Compiler, den Rückgabewert direkt im Speicher des Aufrufers zu konstruieren, wodurch Kopien/Moves vollständig vermieden werden. C++17 macht RVO für Prvalues obligatorisch. Schreiben Sie niemals return std::move(local)—es hemmt NRVO und erzwingt einen (langsameren) Move. Geben Sie einfach das Local nach Namen zurück und lassen Sie den Compiler optimieren.

cpp
std::vector<int> makeVec() {
    std::vector<int> v;
    v.push_back(1); v.push_back(2);
    return v;  // NRVO: no copy, no move!
}

std::string makeStr() {
    return std::string("hello");  // RVO
}

// C++17 guarantees RVO (mandatory copy elision) for prvalues
std::vector<int> v = makeVec();  // constructed in place

// Don't std::move return values—it disables RVO!
std::vector<int> bad() {
    std::vector<int> v;
    return std::move(v);  // WORSE: forces move, blocks NRVO
}
15

Nebenläufigkeit (thread, mutex, async)

std::thread-Grundlagen

std::thread startet einen neuen OS-Thread. Sie MÜSSEN join() (warten) oder detach() (unabhängig laufen lassen) aufrufen, bevor das Thread-Objekt zerstört wird, sonst wird std::terminate aufgerufen. Argumente werden standardmäßig als Wert übergeben—verwenden Sie std::ref für Referenzen, std::move für Move-only-Typen. Bevorzugen Sie join, es sei denn, Sie haben einen klaren Grund zu detach.

cpp
#include <thread>
#include <iostream>

void worker(int id) {
    std::cout << "Thread " << id << "\n";
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2([]{ std::cout << "Lambda thread\n"; });

    t1.join();   // wait for t1 to finish
    t2.join();   // must join or detach before destruction
    // t1.detach();  // runs independently (risky)

    // Hardware concurrency hint
    unsigned n = std::thread::hardware_concurrency();
}

Mutex und Lock Guard

Schützen Sie geteilte Daten immer mit einem Mutex. std::lock_guard ist das einfachste RAII-Lock—akquiriert bei Konstruktion, gibt bei Destruktion frei. std::scoped_lock (C++17) sperrt mehrere Mutexes sicher mit einem Deadlock-Vermeidungs-Algorithmus. std::unique_lock bietet mehr Flexibilität (manuelles Lock/Unlock, verzögertes Sperren) zur Verwendung mit Condition Variables.

cpp
#include <mutex>
#include <vector>

std::mutex mtx;
std::vector<int> shared;

void safePush(int val) {
    // RAII lock: unlocks even if exception thrown
    std::lock_guard<std::mutex> lock(mtx);
    shared.push_back(val);
}  // lock released here

// std::scoped_lock (C++17) locks multiple mutexes deadlock-free
std::mutex m1, m2;
void transfer() {
    std::scoped_lock lock(m1, m2);  // atomic
}

// std::unique_lock: lockable/unlockable, movable
std::unique_lock<std::mutex> ul(mtx);
ul.unlock();
ul.lock();

Condition Variables

condition_variable lässt Threads auf eine Bedingung warten. Verwenden Sie immer ein Prädikat mit wait(), um spurious Wakeups zu behandeln. Der Mutex muss von einem unique_lock gehalten werden, wenn wait() aufgerufen wird, der ihn während des Wartens freigibt und vor der Rückkehr neu akquiriert. notify_one weckt einen Wartenden, notify_all weckt alle. Dieses Muster implementiert Thread-sichere Queues und Producer-Consumer-Pipelines.

cpp
#include <condition_variable>
#include <queue>
#include <thread>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> tasks;

void producer() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        tasks.push(42);
    }
    cv.notify_one();  // wake one waiting consumer
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return !tasks.empty(); });  // predicate prevents spurious wakeup
    int task = tasks.front();
    tasks.pop();
}

std::async und Futures

std::async ist eine High-Level-Methode, Tasks asynchron auszuführen und gibt ein Future zurück. std::launch::async erzwingt einen neuen Thread; std::launch::deferred läuft träge bei get(). Die Standard-Policy kann beides wählen—seien Sie explizit für vorhersagbares Verhalten. Für mehr Kontrolle verwenden Sie std::promise/future-Paare. Rufen Sie immer get() auf einem Future vor der Destruktion auf, sonst kann der Destruktor blockieren.

cpp
#include <future>
#include <iostream>

int slowComputation() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 42;
}

// async policy: may run in new thread or deferred
auto fut = std::async(std::launch::async, slowComputation);
// do other work...
int result = fut.get();  // blocks until ready

// std::launch::async: definitely new thread
// std::launch::deferred: lazy, runs on get()
// std::launch::async | deferred: implementation chooses

// std::promise for manual control
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread([&p]{ p.set_value(10); }).detach();
f.get();  // 10

Atomare Operationen

std::atomic bietet Lock-free Thread-sichere Operationen für primitive Typen. Schwerer als ein einfaches int, aber viel leichter als Mutex für einfache Zähler/Flags. Memory Ordering beeinflusst Sichtbarkeit: relaxed (keine Ordnung), acquire/release (Paar für Synchronisation), seq_cst (Standard, stärkste). Verwenden Sie Atomics für Zähler/Flags; verwenden Sie Mutex für komplexe kritische Abschnitte.

cpp
#include <atomic>
#include <iostream>

std::atomic<int> counter{0};
std::atomic<bool> ready{false};

void worker() {
    while (!ready.load(std::memory_order_acquire));
    counter.fetch_add(1, std::memory_order_relaxed);
}

// Compare-and-swap (CAS) loop
std::atomic<int> val{0};
int expected = 0;
bool success = val.compare_exchange_weak(
    expected, 1,
    std::memory_order_acq_rel);

// Atomic is lock-free for most primitive types
static_assert(std::atomic<int>::is_always_lock_free);

// std::atomic_flag: guaranteed lock-free, for spinlocks
std::atomic_flag spin = ATOMIC_FLAG_INIT;
while (spin.test_and_set(std::memory_order_acquire));  // acquire
spin.clear(std::memory_order_release);  // release
16

Template-Metaprogrammierung

Template-Spezialisierung

Template-Spezialisierung bietet benutzerdefinierte Implementierungen für spezifische Typen. Vollständige Spezialisierung fixiert alle Template-Parameter. Partielle Spezialisierung (nur für Klassen-Templates) spezialisiert einige Parameter, während andere generisch bleiben. Schwer verwendet in Type Traits, std::vector<bool>, und beim Optimieren für bekannte Typen.

cpp
// Primary template
template <typename T>
struct TypeName {
    static const char* get() { return "unknown"; }
};

// Full specialization for int
template <>
struct TypeName<int> {
    static const char* get() { return "int"; }
};

// Full specialization for const char*
template <>
struct TypeName<const char*> {
    static const char* get() { return "string"; }
};

std::cout << TypeName<int>::get();           // "int"
std::cout << TypeName<double>::get();        // "unknown"
std::cout << TypeName<const char*>::get();   // "string"

SFINAE und enable_if

SFINAE (Substitution Failure Is Not An Error) lässt Sie Template-Overloads basierend auf Typeigenschaften aktivieren/deaktivieren. std::enable_if definiert bedingt einen Typ. Wenn die Substitution fehlschlägt, wird der Overload stillschweigend entfernt, statt einen Fehler zu verursachen. C++17's if constexpr und C++20 Concepts ersetzen SFINAE oft mit saubererer Syntax.

cpp
#include <type_traits>

// SFINAE: Substitution Failure Is Not An Error
// Only enable this overload if T is integral
template <typename T,
          typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T x) { std::cout << "integral\n"; }

// Only enable if T is floating point
template <typename T,
          std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void process(T x) { std::cout << "floating\n"; }

process(10);    // integral
process(3.14);  // floating

// void_t trick (C++17) for detecting member existence
template <typename T, typename = void>
struct has_size : std::false_type {};
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
    : std::true_type {};

if constexpr (C++17)

if constexpr wertet die Bedingung zur Kompilierzeit aus und verwirft den falschen Zweig vollständig (nicht einmal typgeprüft). Dies ersetzt viele SFINAE-Patterns mit viel saubererem Code. Besonders nützlich für Template-Rekursion (der Basisfall beendet die Rekursion) und für Branching auf Type Traits ohne Instanziierungsfehler.

cpp
#include <type_traits>
template <typename T>
auto getValue(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;  // only compiled if T is a pointer
    } else if constexpr (std::is_integral_v<T>) {
        return t * 2;
    } else {
        return t;
    }
}

int x = 5;
int* p = &x;
getValue(x);   // 10
getValue(p);   // 5
getValue(3.14);  // 3.14

// Compile-time recursion with termination
template <int N>
constexpr int factorial() {
    if constexpr (N <= 1) return 1;
    else return N * factorial<N - 1>();
}

Variadische Templates und Fold-Ausdrücke

Variadische Templates akzeptieren jede Anzahl von Argumenten via Parameter Packs (typename... Args). C++17 Fold-Ausdrücke wenden einen Operator auf alle Pack-Elemente an: unärer Fold (... op pack), binärer Fold (init op ... op pack). Vor C++17 brauchten Sie Rekursion mit einem Basisfall. Variadische Templates treiben std::make_unique, std::tuple, printf-ähnliche Funktionen an.

cpp
#include <iostream>

// Parameter pack
template <typename... Args>
void print(Args... args) {
    // C++17 fold expression
    ((std::cout << args << " "), ...);
    std::cout << "\n";
}

print(1, "hello", 3.14, 'x');  // 1 hello 3.14 x

// Sum with fold
template <typename... T>
auto sum(T... args) {
    return (args + ...);  // binary fold: ((a+b)+c)+d
}

// Sum with initial value
template <typename... T>
auto sumFrom0(T... args) {
    return (0 + ... + args);  // left fold with init
}

// Base case recursion (pre-C++17)
template <typename T>
void printOne(T t) { std::cout << t; }

Concepts (C++20)

Concepts (C++20) ersetzen SFINAE durch lesbare, absichtsoffenbarende Beschränkungen. Sie produzieren viel bessere Fehlermeldungen als SFINAE. Definieren Sie Concepts mit concept Name = constraint;. Verwenden Sie sie in Template-Parametern, requires-Klauseln oder abgekürzten Templates (auto mit Concept). Die Standardbibliothek bietet viele nützliche Concepts in <concepts>.

cpp
#include <concepts>

// Define a concept
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// Use as constraint
template <Numeric T>
T add(T a, T b) { return a + b; }

// Requires clause
template <typename T>
requires requires(T t) { t.size(); }
auto getSize(const T& t) { return t.size(); }

// Concepts in abbreviated function templates
void process(std::integral auto x) { /* ... */ }
void process(std::ranges::range auto& r) { /* ... */ }

// Standard concepts: integral, floating_point, equality_comparable,
// totally_ordered, copyable, movable, default_initializable, etc.
17

Lambda-Ausdrücke Vertieft

Capture-Modi

Lambdas capturen Variablen aus dem umschließenden Scope. [=] capturet alles als Wert, [&] als Referenz—bequem, aber fehleranfällig (hängende Referenzen, unbeabsichtigte Captures). Bevorzugen Sie explizite Captures [x, &y] für Klarheit. Init-Captures [name = expr] (C++14) erlauben Umbenennen, Verschieben und Berechnen von gecaptureten Werten. Captchen Sie [&] sorgfältig—Lambdas, die den Scope überleben, verursachen hängende Referenzen.

cpp
int x = 10, y = 20;
std::vector<int> v;

auto a = [] { /* no capture */ };
auto b = [x] { return x; };           // capture x by value
auto c = [&x] { x = 100; };           // capture x by reference
auto d = [=] { return x + y; };       // capture all by value
auto e = [&] { x = 1; y = 2; };       // capture all by reference
auto f = [x, &y] { return x + y; };   // mixed
auto g = [=, &x] { x = 1; return y; }; // default value, x by ref
auto h = [this] { return member; };   // capture this (C++17: *this)
auto i = [x = x + 5] { return x; };   // init capture (C++14)

// Best practice: capture only what you need, explicitly

Generische Lambdas (C++14)

Generische Lambdas verwenden auto-Parameter (C++14) oder explizite Template-Parameter (C++20). Sie sind im Wesentlichen compiler-generierte Template-operator()-Overloads. C++20 Template-Lambdas lassen Sie direkt auf den Typ-Parameter T zugreifen. Rekursive Lambdas benötigen std::function (oder C++23 deducing this), weil ein einfaches auto-Lambda nicht vor der Kenntnis seines Typs auf sich selbst verweisen kann.

cpp
// auto parameters (C++14)
auto add = [](auto a, auto b) { return a + b; };
add(1, 2);        // int
add(1.5, 2.5);    // double
add(std::string("a"), std::string("b"));

// Template lambdas (C++20)
auto getSize = []<typename T>(const std::vector<T>& v) {
    return v.size();
};

// Perfect forwarding in lambdas (C++20)
auto wrapper = []<typename T>(T&& arg) {
    return process(std::forward<T>(arg));
};

// Recursive lambda with std::function
#include <functional>
std::function<int(int)> fib = [&](int n) {
    return n < 2 ? n : fib(n - 1) + fib(n - 2);
};

Mutable und zustandsbehaftete Lambdas

Standardmäßig sind als Wert gecapturete Variablen in einem Lambda const—der Aufrufoperator() ist const. Das Schlüsselwort mutable entfernt diese Constness und erlaubt die Modifikation von als-Wert-gecaptureten Variablen (Modifikation der Kopie, nicht des Originals). Dies ermöglicht zustandsbehaftete Lambdas wie Zähler. Jede Kopie eines Lambdas hat ihren eigenen gecaptureten Zustand. Verwenden Sie std::function, um Lambdas unterschiedlicher Typen in Containern zu speichern.

cpp
#include <functional>
int counter = 0;

// Without mutable, value-captured vars are const
auto inc = [counter]() mutable {
    return ++counter;  // modifies the captured copy
};
inc(); inc();
std::cout << counter;  // still 0 (original unchanged)

// Stateful counter with std::function
auto makeCounter(int start) {
    return [count = start]() mutable { return ++count; };
}
auto c = makeCounter(10);
c(); c();  // 11, 12

// Lambda as callback storage
std::vector<std::function<void()>> callbacks;
callbacks.push_back([&]{ std::cout << "click"; });

Lambdas mit STL-Algorithmen

Lambdas glänzen als Prädikate/Komparatoren für STL-Algorithmen. Sie ersetzen den alten Funktor-Ansatz (Funktionsobjekt) durch inline, lesbaren Code. Häufige Verwendungen: Komparatoren für sort, Prädikate für find_if/remove_if/copy_if, Transformationen für transform. C++20 Ranges machen dies noch sauberer mit dem Pipe-Operator für komponierbare Transformationen.

cpp
#include <algorithm>
#include <vector>

std::vector<int> v = {5, 2, 8, 1, 9, 3};

// Sort descending
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a > b;
});

// Find first even
auto it = std::find_if(v.begin(), v.end(),
    [](int x) { return x % 2 == 0; });

// Remove odds (erase-remove idiom)
v.erase(std::remove_if(v.begin(), v.end(),
    [](int x) { return x % 2 != 0; }), v.end());

// Transform
std::transform(v.begin(), v.end(), v.begin(),
    [](int x) { return x * x; });

// C++20 ranges: cleaner
auto evens = v | std::views::filter([](int x){ return x%2==0; })
               | std::views::transform([](int x){ return x*2; });

Higher-Order-Funktionen

Lambdas ermöglichen funktionale Programmiermuster in C++. Funktionen können Lambdas (Closures) zurückgeben oder als Parameter akzeptieren. std::function wrapt jeden Callable für typgelöschte Speicherung. Funktionskomposition baut Pipelines durch Ketten von Lambdas. Dieser Stil ist mächtig für Callbacks, Event-Handler und Datentransformations-Pipelines, obwohl template-basierte Ansätze std::function-Overhead vermeiden.

cpp
#include <functional>
// Function returning a function
auto makeMultiplier(int factor) {
    return [factor](int x) { return x * factor; };
}
auto doubleIt = makeMultiplier(2);
auto tripleIt = makeMultiplier(3);
doubleIt(5);  // 10
tripleIt(5);  // 15

// Function taking a function
template <typename F>
void applyTwice(int x, F f) {
    std::cout << f(f(x));
}
applyTwice(5, [](int x){ return x + 3; });  // 11

// Composition
auto compose = [](auto f, auto g) {
    return [f, g](auto x) { return f(g(x)); };
};
auto addOne = [](int x){ return x + 1; };
auto square = [](int x){ return x * x; };
auto sqThenAdd = compose(addOne, square);
sqThenAdd(3);  // 10
18

Namespaces & Module

Namespace-Grundlagen

Namespaces verhindern Namenskollisionen in großen Projekten. Sie können verschachtelt und für Bequemlichkeit aliasiert werden. Anonyme Namespaces geben Symbolen interne Bindung (nur in der aktuellen Übersetzungseinheit sichtbar) und ersetzen das C-Stil-static-Schlüsselwort für diesen Zweck. Vermeiden Sie using namespace in Headern—es verschmutzt jede einbindende Datei.

cpp
namespace math {
    double pi = 3.14159;
    double square(double x) { return x * x; }

    namespace geometry {  // nested
        double circleArea(double r) { return pi * square(r); }
    }
}

// Usage
double a = math::square(5);
double area = math::geometry::circleArea(2.0);

// Namespace alias
namespace geo = math::geometry;
geo::circleArea(1.0);

// Anonymous namespace: internal linkage (like 'static')
namespace {
    int internalVar = 42;  // only visible in this file
}

using-Deklarationen und -Direktiven

using-Deklaration (using std::cout) importiert einen einzelnen Namen—sicher und empfohlen. using-Direktive (using namespace std) importiert alles—bequem, aber riskant, besonders in Headern (verursacht Namenskonflikte und Mehrdeutigkeit). Beschränken Sie using-Direktiven auf kleinen Scope (Funktions-/Implementierungsdateien) und niemals in Headern. C++20 using enum importiert alle Enumeratoren eines Scoped Enum.

cpp
#include <iostream>
#include <vector>

// using declaration: brings one name
using std::cout;
using std::vector;
cout << "hello";
vector<int> v;

// using directive: brings entire namespace
using namespace std;  // brings ALL of std
// AVOID in headers! Pollutes global namespace.

// Namespace-scoped using (safe)
namespace mylib {
    using std::string;  // only affects mylib
    using std::vector;
    string s;
}

// C++20: using enum
enum class Color { Red, Green, Blue };
void print() {
    using enum Color;
    auto c = Red;  // no Color:: needed here
}

Argument-Dependent Lookup (ADL)

ADL (Koenig-Lookup) findet freie Funktionen basierend auf den Namespaces ihrer Argumente. Deshalb funktioniert std::cout << x ohne std::operator<<—der Compiler sucht in x's Namespace. ADL ist essenziell für Operator-Overloads und Customization Points wie swap. Das 'using std::swap; swap(a,b);'-Muster lässt benutzerdefinierte Typen optimierte Swaps bereitstellen, während auf std::swap zurückgefallen wird.

cpp
namespace mylib {
    struct Widget {
        int value;
    };
    // Operator overload found via ADL
    std::ostream& operator<<(std::ostream& os, const Widget& w) {
        return os << "Widget(" << w.value << ")";
    }

    void helper(const Widget&) {}
}

int main() {
    mylib::Widget w{42};
    // ADL: finds operator<< in mylib without qualification
    std::cout << w;  // works! no mylib:: needed

    // ADL applies to free functions too
    helper(w);  // wait—needs mylib::helper unless ADL applies
    mylib::helper(w);  // explicit
}

// swap is the classic ADL use case
namespace ns { struct X {};
    void swap(X&, X&) {}  // customized swap
}
void f(ns::X& a, ns::X& b) {
    using std::swap;
    swap(a, b);  // ADL picks ns::swap if available
}

Inline Namespaces (Versionierung)

Inline Namespaces exponieren ihre Member, als wären sie im umschließenden Namespace. Dies ermöglicht Bibliotheksversionierung: Machen Sie die neueste Version inline, sodass Benutzer sie standardmäßig erhalten, während alte Versionen über explizite Qualifikation zugänglich bleiben. Es wird auch für ABI-Kompatibilität und Feature-Toggling verwendet. Das Ändern, welcher Namespace inline ist, verschiebt die Standardversion, ohne Benutzercode zu modifizieren.

cpp
// Inline namespace members are part of the enclosing namespace
namespace mylib {
    inline namespace v2 {
        struct Widget { int x, y, z; };  // newer version
        void process(Widget) {}
    }

    namespace v1 {
        struct Widget { int x, y; };  // older version
    }
}

// v2::Widget is accessible as mylib::Widget (inline)
mylib::Widget w;  // actually mylib::v2::Widget
mylib::process(w);

// Explicitly use v1 if needed
mylib::v1::Widget oldW;

// ABI versioning: change which namespace is inline
// to change default version without breaking old code

C++20 Module

C++20 Module ersetzen #include durch ein schnelleres, robusteres System. export module deklariert ein Modul; export markiert sichtbare Deklarationen. Module werden einmal kompiliert (nicht pro Übersetzungseinheit neu geparst), was Build-Zeiten drastisch verbessert. Sie vermeiden Makro-Verschmutzung und Header-Reihenfolgen-Probleme. Die Adoption ist allmählich—Toolchain-Unterstützung (CMake, Build-Systeme) reift noch 2024.

cpp
// math.cppm (module interface unit)
export module math;

export double pi = 3.14159;

export double square(double x) {
    return x * x;
}

// Internal (not exported)
double internalHelper(double x) {
    return x * 2;
}

// main.cpp
import math;
import std;  // standard library module (C++23)

int main() {
    return square(pi);  // 9.87
    // internalHelper(3);  // ERROR: not exported
}

// Module partitions (sub-modules)
export module math:geometry;
export double circleArea(double r);
19

Präprozessor & Makros

Include-Guards und #pragma once

Include-Guards verhindern, dass ein Header mehrfach in einer Übersetzungseinheit verarbeitet wird, was Neudefinitionsfehler vermeidet. #ifndef/#define/#endif ist Standard und portabel. #pragma once ist einfacher und vermeidet Makronamenskollisionen, ist aber technisch nicht standard (von allen gängigen Compilern unterstützt). Moderner Code verwendet oft #pragma once der Einfachheit halber.

cpp
// Traditional include guard (header.h)
#ifndef MY_HEADER_H
#define MY_HEADER_H

// declarations here
class Widget { /* ... */ };

#endif  // MY_HEADER_H

// Alternative: #pragma once (non-standard but widely supported)
#pragma once

class Widget { /* ... */ };

// #pragma once pros: simpler, no risk of macro name collision
// Include guards pros: standard, works everywhere
// Both prevent multiple inclusion in a single TU

Funktionsähnliche Makros

Funktionsähnliche Makros sind Textsubstitution. KLAMMERN Sie immer jedes Argument und den gesamten Ausdruck, um Vorrang-Bugs zu vermeiden. Das do { ... } while (0)-Idiom lässt ein Makro sich wie eine einzelne Anweisung verhalten. Makros haben keine Typprüfung, keinen Scope und können Seiteneffekte haben (MAX(i++, j++) inkrementiert zweimal). Bevorzugen Sie constexpr/inline/templates in modernem C++.

cpp
// Basic macro
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))

int m = MAX(3, 5);      // ((3) > (5) ? (3) : (5))
int s = SQUARE(4);      // ((4) * (4))

// ALWAYS parenthesize arguments!
#define BAD(x) x * x
BAD(1 + 2);  // 1 + 2 * 1 + 2 = 5, not 9!

// Multi-line macro with do-while
#define LOG(msg) do { \
    std::cerr << __FILE__ << ":" << __LINE__ \
              << " " << msg << "\n"; \
} while (0)

// Variadic macro (C++11)
#define PRINT(...) printf(__VA_ARGS__)
PRINT("x=%d, y=%d\n", x, y);

Stringifizierung und Verkettung

# (Stringifizierung) konvertiert ein Makro-Argument in ein String-Literal. ## (Token-Pasting) verkettet zwei Tokens zu einem. Der zweistufige STR/XSTR-Trick expandiert zuerst Makros und stringifiziert sie dann. Diese Operatoren sind essenziell für Codegenerierung, Logging-Makros und X-Makros. Modernes C++ ersetzt diese oft durch constexpr-Funktionen, aber sie bleiben nützlich für Kompilierzeit-String/Token-Generierung.

cpp
// # (stringification) turns a macro argument into a string literal
#define STR(x) #x
#define XSTR(x) STR(x)
const char* s1 = STR(hello world);  // "hello world"
const char* s2 = STR(42);           // "42"

// Two-level macro to expand macros before stringifying
#define VERSION 100
const char* v1 = STR(VERSION);   // "VERSION"
const char* v2 = XSTR(VERSION);  // "100"

// ## (token pasting) concatenates tokens
#define CONCAT(a, b) a ## b
#define MAKE_VAR(n) var_ ## n
int MAKE_VAR(1) = 10;  // int var_1 = 10;
int CONCAT(foo, bar) = 5;  // int foobar = 5;

// Useful for generating unique names
#define UNIQUE(prefix) prefix ## __LINE__
int UNIQUE(tmp_) = 0;  // int tmp_42 = 0; (if on line 42)

Bedingte Kompilierung

Bedingte Kompilierung (#if, #ifdef, #ifndef, #elif, #else, #endif) schließt Code zur Kompilierzeit ein/aus. Verwendet für plattformspezifischen Code, Debug-Builds und Feature-Flags. defined(X) prüft, ob X definiert ist (Wert irrelevant). #error bricht die Kompilierung mit einer Meldung ab. Bevorzugen Sie constexpr if, wenn möglich—es ist typsicher und der Code wird immer kompiliert (erfasst Fehler in allen Konfigurationen).

cpp
#define DEBUG 1
#define PLATFORM "windows"

#if DEBUG
    std::cout << "Debug mode\n";
#endif

#ifdef DEBUG
    logDebug("entered function");
#endif

#ifndef NDEBUG
    assert(ptr != nullptr);
#endif

#if defined(_WIN32) || defined(_WIN64)
    #include <windows.h>
#elif defined(__linux__)
    #include <unistd.h>
#elif defined(__APPLE__)
    #include <TargetConditionals.h>
#endif

// Check C++ standard
#if __cplusplus >= 202002L
    #define CPP20_OR_LATER
#elif __cplusplus >= 201703L
    #define CPP17_OR_LATER
#endif

// #error and #warning
#if !defined(VERSION)
    #error "VERSION must be defined"
#endif

Vordefinierte Makros und __VA_OPT__

Vordefinierte Makros bieten Kompilierzeit-Info: __FILE__/__LINE__ für Logging, __cplusplus für Standard-Version, __func__ für Funktionsnamen (es ist ein Bezeichner, kein Makro). __VA_OPT__ (C++20) fügt bedingt ein Komma nur ein, wenn __VA_ARGS__ nicht leer ist, was das Problem des nachfolgenden Kommas in variadischen Makros behebt. __has_include (C++17) ermöglicht optionales Header-Einbinden für Portabilität.

cpp
// Standard predefined macros
std::cout << __FILE__;       // source file path
std::cout << __LINE__;       // current line number
std::cout << __DATE__;       // compilation date "Mmm dd yyyy"
std::cout << __TIME__;       // compilation time "hh:mm:ss"
std::cout << __cplusplus;    // C++ standard version
std::cout << __func__;       // current function name (not macro)

// Compiler-specific
#ifdef __GNUC__
    std::cout << __GNUC__ << "." << __GNUC_MINOR__;
#endif
#ifdef _MSC_VER
    std::cout << "MSVC " << _MSC_VER;
#endif

// __VA_OPT__ (C++20): expands to its arg if variadic has args
#define LOG(fmt, ...) \
    printf(fmt __VA_OPT__(,) __VA_ARGS__)
LOG("plain");              // printf("plain")
LOG("x=%d", x);            // printf("x=%d", x)

// __has_include (C++17): check if header exists
#if __has_include(<optional>)
    #include <optional>
#endif
20

Design Patterns in C++

Singleton (Meyers' Singleton)

Singleton stellt sicher, dass eine Klasse eine Instanz mit globalem Zugriff hat. Meyers' Singleton (statische lokale Variable) ist die sauberste C++-Implementierung—Thread-sichere Initialisierung ist seit C++11 garantiert. Löschen Sie Copy-Operationen, um Duplikation zu verhindern. Singletons sind umstritten (globaler Zustand, schwer zu testen); bevorzugen Sie Dependency Injection, wenn machbar. Verwenden Sie, wenn genuinely eine Instanz benötigt wird (Logger, Config, Hardware-Schnittstelle).

cpp
class Logger {
public:
    static Logger& instance() {
        // Meyers' singleton: thread-safe in C++11+
        static Logger inst;
        return inst;
    }
    void log(const std::string& msg) {
        std::cout << "[LOG] " << msg << "\n";
    }
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
private:
    Logger() = default;
};

// Usage
Logger::instance().log("started");

// Avoid: lazy init with new + manual sync (error-prone)
// The static local is initialized once, thread-safely

RAII (Resource Acquisition Is Initialization)

RAII ist C++' wichtigstes Idiom: Akquirieren Sie Ressourcen in Konstruktoren, geben Sie sie in Destruktoren frei. Dies garantiert Cleanup, selbst wenn Exceptions geworfen werden. Smart Pointer, std::lock_guard, std::fstream und std::vector verwenden alle RAII. Der Destruktor läuft während Stack-Unwinding, was Exception-sicheren Code natürlich macht. RAII eliminiert manuelles new/delete, lock/unlock, open/close—umarmen Sie es überall.

cpp
// RAII: resource tied to object lifetime
class FileHandle {
    FILE* fp;
public:
    explicit FileHandle(const char* path) : fp(fopen(path, "r")) {
        if (!fp) throw std::runtime_error("open failed");
    }
    ~FileHandle() { if (fp) fclose(fp); }
    FILE* get() { return fp; }
    // disable copy to prevent double-close
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    // allow move
    FileHandle(FileHandle&& o) noexcept : fp(o.fp) { o.fp = nullptr; }
};

{
    FileHandle f("data.txt");
    // use f.get()
}  // fclose called automatically, even on exception

Pimpl-Idiom (Pointer to Implementation)

Pimpl (Pointer to Implementation) verbirgt Implementierungsdetails hinter einem Zeiger. Vorteile: schnellere Kompilierung (private Member erscheinen nicht im Header), echte ABI-Stabilität (Impl ändern bricht nicht ABI), reduzierte Include-Abhängigkeiten. Der Destruktor muss in der .cpp-Datei definiert werden, weil unique_ptr<Impl> Impl vollständig benötigt. Häufig in Bibliotheksdesign für stabile APIs.

cpp
// widget.h - public interface
class Widget {
public:
    Widget();
    ~Widget();  // must define in .cpp (unique_ptr needs complete type)
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;
    void doSomething();
private:
    class Impl;  // forward declaration
    std::unique_ptr<Impl> pimpl;
};

// widget.cpp - implementation
#include "widget.h"
#include <vector>
class Widget::Impl {
public:
    std::vector<int> data;
    void doSomething() { /* ... */ }
};
Widget::Widget() : pimpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
void Widget::doSomething() { pimpl->doSomething(); }

Observer-Pattern

Observer lässt Subjekte Abonnenten über Änderungen benachrichtigen, ohne Kopplung. std::function macht es einfach, Lambdas, Member-Funktionen und Funktoren zu akzeptieren. Für Produktionscode ziehen Sie einen Token-basierten Abbestell-Mechanismus in Betracht (geben Sie eine ID beim Abonnieren zurück, verwenden Sie sie zum Entfernen). Achten Sie auf hängende Referenzen, wenn Observer [&] capturen und das Subjekt überleben. Qt's Signals/Slots und C#-Events sind ausgereifte Implementierungen.

cpp
#include <functional>
#include <vector>
#include <string>

class Subject {
    std::vector<std::function<void(const std::string&)>> observers;
public:
    void subscribe(std::function<void(const std::string&)> cb) {
        observers.push_back(cb);
    }
    void notify(const std::string& event) {
        for (auto& cb : observers) cb(event);
    }
};

// Usage
Subject s;
s.subscribe([](const std::string& e){ std::cout << "A: " << e; });
s.subscribe([](const std::string& e){ std::cout << "B: " << e; });
s.notify("hello");  // both callbacks invoked

// For removal, use a token/ID system or observer objects
// instead of std::function

CRTP (Curiously Recurring Template Pattern)

CRTP (Derived : Base<Derived>) erreicht statischen Polymorphismus—die Basisklasse passt Verhalten über den abgeleiteten Typ an. Kein Virtual-Function-Overhead. Verwendet, um Funktionalität (Comparable, Iterable) zu abgeleiteten Klassen via Mixins hinzuzufügen. Downcasting via static_cast ist sicher, weil das Template den abgeleiteten Typ garantiert. CRTP treibt std::enable_shared_from_this, std::iterator und viele Policy-basierte Designs an.

cpp
// CRTP: class Derived : public Base<Derived>
template <typename Derived>
struct Comparable {
    bool operator==(const Derived& other) const {
        return static_cast<const Derived*>(this)->equalTo(other);
    }
    bool operator!=(const Derived& other) const {
        return !(*this == other);
    }
};

struct Point : Comparable<Point> {
    int x, y;
    bool equalTo(const Point& o) const { return x == o.x && y == o.y; }
};

Point a{1, 2}, b{1, 2};
a == b;  // true (uses Comparable::operator==)
a != b;  // false

// Static polymorphism (no virtual overhead)
template <typename T>
void draw(const T& shape) {
    static_cast<const T&>(shape).drawImpl();
}
21

Smart Pointer Vertieft

unique_ptr

unique_ptr ist alleiniger Eigentümer seines Objekts. Kann nicht kopiert, nur verschoben werden. Löscht automatisch beim Verlassen des Scope. make_unique ist die bevorzugte Erstellungsmethode. Null-Overhead gegenüber rohen Zeigern.

cpp
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1;  // Error: cannot copy
std::unique_ptr<int> p3 = std::move(p1);  // OK: transfer ownership
// p1 is now nullptr

shared_ptr

shared_ptr erlaubt mehrere Eigentümer via Referenzzählung. use_count() zeigt die Anzahl der Eigentümer. Thread-sicher für den Zähler, aber nicht für das Objekt. Verwenden Sie make_shared für Effizienz (einzelne Allokation).

cpp
auto p1 = std::make_shared<int>(42);
auto p2 = p1;  // OK: shared ownership
std::cout << p1.use_count();  // 2
// Reference counting: deleted when count reaches 0
// Thread-safe for reference count, not for the object

weak_ptr

weak_ptr ist eine nicht-besitzende Referenz auf shared_ptr. Verhindert Zirkularreferenzen (Speicherlecks). lock() versucht die Konvertierung zu shared_ptr. Verwenden Sie expired(), um zu prüfen, ob das Objekt noch existiert. Beeinflusst nicht den Referenzzähler.

cpp
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;  // Does not increase count
if (auto locked = weak.lock()) {  // Try to get shared_ptr
    std::cout << *locked;  // Use the object
} else {
    std::cout << "Object deleted";
}

Benutzerdefinierter Deleter

Benutzerdefinierte Deleter erlauben unique_ptr, Nicht-Speicher-Ressourcen wie Datei-Handles, Sockets und C-APIs zu verwalten. Der Deleter-Typ ist Teil des unique_ptr-Typs. Nützlich für RAII mit C-Bibliotheken.

cpp
std::unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "r"), fclose);
// Or with lambda
auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> file2(fopen("test.txt", "r"), deleter);

enable_shared_from_this

enable_shared_from_this erlaubt einem Objekt, sicher einen shared_ptr zu sich selbst zu erhalten. Das direkte Aufrufen von shared_ptr(this) würde einen zweiten Referenzzähler erstellen und Double-Deletion verursachen. Erben Sie von enable_shared_from_this und verwenden Sie shared_from_this().

cpp
class Node : public std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getPtr() {
        return shared_from_this();  // Safe
        // return std::shared_ptr<Node>(this);  // BUG: double delete
    }
};
22

Move-Semantik Vertieft

Move-Konstruktor

Move-Konstruktoren stehlen Ressourcen statt zu kopieren. noexcept ist wichtig: STL-Container verschieben (nicht kopieren) nur, wenn der Move noexcept ist. Lassen Sie das verschobene Objekt immer in einem gültigen Zustand.

cpp
class Buffer {
    int* data; size_t size;
public:
    Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr; other.size = 0;  // Leave in valid state
    }
    Buffer& operator=(Buffer&& other) noexcept {
        delete[] data;
        data = other.data; size = other.size;
        other.data = nullptr; other.size = 0;
        return *this;
    }
};

std::move

std::move ist ein Cast zu Rvalue-Referenz. Es führt den Move selbst nicht aus. Der Move-Konstruktor oder Zuweisungsoperator erledigt die eigentliche Arbeit. Nach std::move befindet sich das Quellobjekt in einem gültigen, aber unspezifizierten Zustand.

cpp
std::string s1 = "Hello";
std::string s2 = std::move(s1);  // s1 is now empty
// std::move does not move anything!
// It casts to an rvalue reference, enabling move
// The actual move happens in the constructor/assignment

Perfect Forwarding

Perfect Forwarding erhält die Wertkategorie von Argumenten. T&& in einem Template ist eine Forwarding-Referenz, die an sowohl Lvalues als auch Rvalues bindet. std::forward<T> castet zurück zur ursprünglichen Kategorie. Essenziell für Factory-Funktionen und Wrapper.

cpp
template<typename T, typename Arg>
auto make(Arg&& arg) {
    return std::make_shared<T>(std::forward<Arg>(arg));
}
// std::forward preserves value category:
// lvalue stays lvalue, rvalue stays rvalue
// Arg&& is a forwarding reference (not rvalue reference)

RVO & NRVO

RVO (Return Value Optimization) und NRVO (Named RVO) eliminieren Kopien, indem das Objekt in-place konstruiert wird. Compiler führen dies automatisch durch. Die Verwendung von std::move auf einem lokalen Rückgabewert verhindert NRVO und kann verschlechtern. Vertrauen Sie dem Compiler.

cpp
std::string create() {
    std::string s = "Hello";
    return s;  // NRVO: no copy, no move
}
std::string s = create();  // RVO: no copy
// With -O2, compilers eliminate the copy/move entirely
// Do NOT use std::move on return of local variable!

Rule of Five

Rule of Five: Wenn Sie einen von Destruktor, Copy-Konstruktor/-Zuweisung oder Move-Konstruktor/-Zuweisung definieren, definieren Sie alle fünf. Dies stellt korrekte Ressourcenverwaltung sicher. Die Rule of Zero wird bevorzugt: Verwenden Sie RAII-Typen (Smart Pointer, Vektoren), um manuelle Verwaltung zu vermeiden.

cpp
class Resource {
public:
    Resource();  // Constructor
    ~Resource();  // Destructor
    Resource(const Resource&);  // Copy constructor
    Resource& operator=(const Resource&);  // Copy assignment
    Resource(Resource&&) noexcept;  // Move constructor
    Resource& operator=(Resource&&) noexcept;  // Move assignment
};
23

Templates Vertieft

Variadische Templates

Variadische Templates akzeptieren jede Anzahl von Argumenten. Fold-Ausdrücke (C++17) vereinfachen das Auspacken. Der rekursive Ansatz funktioniert in C++11. sizeof...(args) gibt die Zählung. Verwendet in tuple, make_shared und printf-Ersatz.

cpp
template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << '\n';  // C++17 fold expression
}
print(1, "hello", 3.14, 'a');  // 1hello3.14a
// Recursive unpacking (pre-C++17)
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
    std::cout << first;
    print(rest...);
}

SFINAE

SFINAE (Substitution Failure Is Not An Error) entfernt ungültige Overloads aus der Betrachtung. enable_if fügt eine Bedingung zum Template hinzu. Wenn die Bedingung false ist, wird die Spezialisierung ignoriert. C++17 if constexpr ist oft sauberer.

cpp
template<typename T,
         typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T value) { /* integer version */ }
template<typename T,
         typename = std::enable_if_t<std::is_floating_point_v<T>>,
         typename = void>  // Different signature
void process(T value) { /* float version */ }

if constexpr

if constexpr (C++17) wertet Bedingungen zur Kompilierzeit aus. Nur der wahre Zweig wird kompiliert, der andere verworfen. Viel sauberer als SFINAE für bedingte Kompilierung. Funktioniert mit auto-Rückgabetyp-Deduktion.

cpp
template<typename T>
auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>)
        return *t;
    else
        return t;
}
// Only the matching branch is compiled
// No SFINAE needed

Concepts (C++20)

Concepts (C++20) beschränken Template-Parameter mit lesbarer Syntax. Sie bieten bessere Fehlermeldungen als SFINAE. Verwenden Sie vorhandene Concepts (integral, floating_point) oder definieren Sie benutzerdefinierte. Concepts können mit && und || kombiniert werden.

cpp
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<Number T>
T add(T a, T b) { return a + b; }
// Or: requires clause
template<typename T> requires Number<T>
T multiply(T a, T b) { return a * b; }

Template-Spezialisierung

Vollständige Spezialisierung bietet eine vollständige Implementierung für einen spezifischen Typ. Partielle Spezialisierung passt für eine Kategorie an (z.B. alle Zeiger). Das primäre Template muss zuerst deklariert werden. Spezialisierungen müssen zur Schnittstelle passen.

cpp
template<typename T>
class Vector { /* general implementation */ };
template<>
class Vector<bool> {  // Full specialization
    // Bit-packed implementation
};
template<typename T>
class Vector<T*> {  // Partial specialization for pointers
    // Pointer-specific implementation
};
24

STL-Algorithmen Vertieft

sort & stable_sort

sort ist O(n log n), nicht stabil. stable_sort erhält die relative Reihenfolge gleicher Elemente. Verwenden Sie einen Komparator für benutzerdefinierte Sortierung. C++20 Ranges erlauben: std::ranges::sort(v, {}, &last_digit).

cpp
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end());  // 1,1,3,4,5,9
std::sort(v.begin(), v.end(), std::greater<>());  // Descending
// Custom comparator
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a % 10 < b % 10;  // Sort by last digit
});

transform & accumulate

transform wendet eine Funktion auf jedes Element an. accumulate faltet Elemente mit einer Operation (Standard +). Der Initialwert bestimmt den Typ. Verwenden Sie std::reduce (C++17) für parallele Reduktion.

cpp
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
    [](int x) { return x * x; });  // 1,4,9,16
int sum = std::accumulate(v.begin(), v.end(), 0);  // 10
int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<>());

find & count

find gibt einen Iterator zur ersten Übereinstimmung oder end() zurück. count gibt die Anzahl der Übereinstimmungen zurück. find_if verwendet ein Prädikat. Alle sind O(n). Für sortierte Bereiche verwenden Sie binary_search, lower_bound, upper_bound (O(log n)).

cpp
std::vector<int> v = {1, 2, 3, 2, 1};
auto it = std::find(v.begin(), v.end(), 2);  // First 2
size_t cnt = std::count(v.begin(), v.end(), 2);  // 2
auto it2 = std::find_if(v.begin(), v.end(),
    [](int x) { return x > 2; });  // First > 2

copy & remove

copy_if kopiert übereinstimmende Elemente. back_inserter hängt an das Ziel an. remove löscht nicht tatsächlich; es verschiebt nicht übereinstimmende Elemente nach vorne und gibt ein neues Ende zurück. erase entfernt den Rest. C++20 fügt std::erase für Container hinzu.

cpp
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> evens;
std::copy_if(v.begin(), v.end(), std::back_inserter(evens),
    [](int x) { return x % 2 == 0; });  // 2,4
// Remove-erase idiom
v.erase(std::remove(v.begin(), v.end(), 3), v.end());
// C++20: std::erase(v, 3);

minmax & clamp

minmax gibt ein Paar (min, max) in einem Durchgang zurück. clamp beschränkt einen Wert auf einen Bereich. min/max akzeptieren Initialisiererlisten oder zwei Werte. Alle akzeptieren benutzerdefinierte Komparatoren. Structured Bindings (C++17) vereinfachen das Ergebnis.

cpp
auto [min, max] = std::minmax({3, 1, 4, 1, 5});  // C++17
int value = std::clamp(15, 0, 10);  // 10 (clamped to max)
int m = std::min({1, 2, 3});
int m2 = std::max({1, 2, 3}, [](int a, int b) { return a < b; });
25

Nebenläufigkeit

std::thread

std::thread erstellt OS-Threads. join() wartet auf Abschluss (blockierend). detach() läuft unabhängig (kann den Ersteller überleben). Ein Thread mit beidem verursacht std::terminate. Übergeben Sie Argumente als Wert oder verwenden Sie std::ref für Referenzen.

cpp
#include <thread>
void task(int n) { /* ... */ }
std::thread t1(task, 42);
std::thread t2([]() { /* lambda */ });
t1.join();  // Wait for completion
t2.detach();  // Run independently
// Always join or detach before destruction

std::mutex

lock_guard ist einfaches RAII: sperrt bei Konstruktion, entsperrt bei Destruktion. unique_lock ist flexibler: kann entsperren/resperrn, verwendet mit Condition Variables. Entsperren Sie niemals manuell mit lock_guard. Verwenden Sie std::scoped_lock für mehrere Mutexes.

cpp
std::mutex mtx;
int shared = 0;
void increment() {
    std::lock_guard<std::mutex> lock(mtx);  // RAII
    ++shared;
}  // Auto-unlock
// std::unique_lock for conditional locking
std::unique_lock<std::mutex> ulock(mtx);
ulock.unlock();  // Manual unlock
ulock.lock();    // Re-lock

std::async & futures

std::async führt eine Funktion asynchron aus und gibt ein Future zurück. get() blockiert und ruft das Ergebnis ab. launch::async erzwingt einen neuen Thread. launch::deferred läuft synchron bei get(). Standard kann beides sein. Exceptions werden durch get() propagiert.

cpp
#include <future>
std::future<int> f = std::async(std::launch::async, []() {
    return 42;
});
int result = f.get();  // Blocks until ready
// std::launch::async: always new thread
// std::launch::deferred: lazy, runs on get()

Condition Variable

condition_variable synchronisiert Threads. wait gibt das Lock frei und blockiert bis benachrichtigt. Das Prädikat behandelt spurious Wakeups. notify_one weckt einen Wartenden, notify_all weckt alle. Modifizieren Sie geteilten Zustand immer unter dem Mutex vor dem Benachrichtigen.

cpp
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// Waiter
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []() { return ready; });  // Spurious wakeups handled
// Notifier
{
    std::lock_guard<std::mutex> lk(mtx);
    ready = true;
}
cv.notify_one();  // Or notify_all()

Atomare Operationen

atomic bietet Lock-free Thread-sichere Operationen. fetch_add/sub sind Read-Modify-Write. compare_exchange implementiert CAS (Compare-and-Swap). memory_order_relaxed ist am schnellsten, aber am schwächsten. seq_cst (Standard) ist am stärksten. Verwenden Sie Atomics für einfache Zähler und Flags.

cpp
#include <atomic>
std::atomic<int> counter{0};
counter++;  // Atomic increment
counter.fetch_add(1, std::memory_order_relaxed);
bool expected = false;
counter.compare_exchange_strong(expected, true);
// Memory orders: relaxed, acquire, release, seq_cst
26

Moderne C++-Features

Structured Bindings

Structured Bindings (C++17) zerlegen Paare, Tuples und Structs. auto& für Referenzen, auto für Kopien. Vereinfacht das Iterieren über Maps. Funktioniert mit jedem Aggregattyp. Viel sauberer als .first/.second.

cpp
std::pair p = {1, "hello"};
auto [num, str] = p;  // C++17
std::map<std::string, int> m = {{"a", 1}, {"b", 2}};
for (const auto& [key, value] : m) {
    std::cout << key << ": " << value;
}

std::optional

optional repräsentiert einen Wert, der existieren kann oder nicht. Besser als Zeiger oder Sentinel-Werte. has_value() oder operator bool prüft. value() wirft, wenn leer, value_or() bietet einen Standard. Verwenden Sie für Funktionen, die möglicherweise kein Ergebnis produzieren.

cpp
#include <optional>
std::optional<int> find(bool found) {
    if (found) return 42;
    return std::nullopt;
}
auto result = find(true);
if (result) std::cout << *result;
// Or: result.value_or(0)

std::variant

variant ist eine typsichere Union. Hält einen von mehreren Typen. visit wendet einen Visitor an (überladen Lambda). index() gibt den aktuellen Typindex zurück. get_if<T> ruft sicher ab. Ersetzt Unions und Vererbung für geschlossene Typhierarchien.

cpp
#include <variant>
std::variant<int, std::string> v;
v = 42;
v = "hello";
std::visit([](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>)
        std::cout << "int: " << arg;
    else if constexpr (std::is_same_v<T, std::string>)
        std::cout << "string: " << arg;
}, v);

Ranges (C++20)

Ranges (C++20) bieten komponierbare, träge Views. Der |-Operator verkettet Operationen. Keine intermediären Container. Views sind träge: Berechnung passiert bei Iteration. Viel lesbarer als verschachtelte Algorithmus-Aufrufe.

cpp
#include <ranges>
namespace rv = std::ranges::views;
auto result = std::vector{1, 2, 3, 4, 5}
    | rv::filter([](int x) { return x % 2 == 0; })
    | rv::transform([](int x) { return x * x; });
// Lazy evaluation: no computation until iterated
for (int x : result) std::cout << x;  // 4, 16

Koroutinen (C++20)

Koroutinen (C++20) ermöglichen Async- und Generator-Patterns. co_yield pausiert und gibt einen Wert zurück. co_await wartet auf eine andere Koroutine. co_return beendet. Der Compiler transformiert Koroutinen in Zustandsautomaten. Benötigt eine Rückgabetyp- (Generator, Task) Implementierung.

cpp
#include <coroutine>
Generator<int> counter() {
    for (int i = 0; ; ++i)
        co_yield i;  // Suspend and yield
}
for (int x : counter()) {
    if (x > 5) break;
    std::cout << x;
}
// co_await: wait for async operation
// co_return: finish coroutine
27

Häufige Fallen

Hängende Zeiger

Hängende Zeiger zeigen auf freigegebenen Speicher. Der Zugriff darauf ist undefiniertes Verhalten. Verwenden Sie Smart Pointer (unique_ptr, shared_ptr), um manuelle Speicherverwaltung zu vermeiden. Wenn Sie rohe Zeiger verwenden, setzen Sie nach delete auf nullptr. Verwenden Sie Tools wie AddressSanitizer zur Erkennung.

cpp
// BUG: dangling pointer
int* p = new int(42);
delete p;
std::cout << *p;  // Undefined behavior
// FIX: use smart pointers
auto p = std::make_unique<int>(42);
// Or set to nullptr after delete
int* p2 = new int(42);
delete p2; p2 = nullptr;

Iterator-Invalidierung

Vector push_back kann reallokieren und alle Iteratoren invalidieren. erase invalidiert Iteratoren ab dem Punkt. list-Iteratoren sind stabil außer für gelöschte Elemente. Prüfen Sie Iterator-Invalidierungsregeln für jeden Container. Im Zweifel verwenden Sie Indizes.

cpp
std::vector<int> v = {1, 2, 3};
// BUG: iterator invalidated
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it == 2) v.push_back(4);  // May invalidate it
}
// FIX: use index
for (size_t i = 0; i < v.size(); ++i) {
    if (v[i] == 2) v.push_back(4);
}

Undefiniertes Verhalten

Undefiniertes Verhalten (UB) erlaubt dem Compiler, alles zu tun. Nicht initialisierte Variablen, Out-of-Bounds-Zugriff, Null-Dereferenzierung, vorzeichenbehafteter Überlauf sind alles UB. Compiler optimieren unter der Annahme, dass kein UB vorliegt, was zu überraschenden Ergebnissen führt. Verwenden Sie -fsanitize=undefined zur Erkennung.

cpp
int x;  // Uninitialized: UB to read
std::cout << x;  // UB
int arr[5];
arr[5] = 0;  // Out of bounds: UB
int* p = nullptr;
*p;  // Null dereference: UB
// Signed overflow: UB (unsigned wraps)
int big = INT_MAX + 1;  // UB

Slicing

Object-Slicing tritt auf, wenn ein abgeleitetes Objekt in einen Basistyp kopiert wird. Die abgeleiteten Teile gehen verloren. Übergeben Sie als Referenz oder Zeiger, um Slicing zu vermeiden. Virtuelle Aufrufe auf einem gesliceten Objekt rufen die Basisversion auf. Übergeben Sie polymorphe Typen immer als Referenz/Zeiger.

cpp
class Base { public: virtual ~Base() {} };
class Derived : public Base { int extra; };
void take(Base b) { /* copies Base part only */ }
Derived d;
take(d);  // Sliced: extra lost
// FIX: pass by reference or pointer
void take(const Base& b) { /* full object */ }

Statische Initialisierung

Die Reihenfolge der statischen Initialisierung über Übersetzungseinheiten hinweg ist undefiniert. Eine Datei kann eine noch nicht initialisierte statische Datei einer anderen verwenden. Beheben Sie mit funktionslokalen Statics (Meyers Singleton): Initialisierung ist träge und Thread-sicher (C++11+). Zugriff über Funktionsaufruf.

cpp
// Static initialization order fiasco
// File1.cpp
int x = computeX();  // May use y
// File2.cpp
int y = computeY();  // May use x
// Order across files is undefined!
// FIX: function-local static
int& getX() {
    static int x = computeX();  // Lazy, thread-safe
    return x;
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.