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.
#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.
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.
#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.
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 6Typumwandlung
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.
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);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.
#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"); // 7Vergleich & 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.
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 occurrenceStringstream
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.
#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=30Raw-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.
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.
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++ -> CZahlen & 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.
#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.
#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.
#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.5Zufallszahlen
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.
#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.
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
}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.
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.
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).
// 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 copyWhile & 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.
int n = 5;
while (n > 0) {
std::cout << n-- << " ";
}
int x;
do {
std::cin >> x;
} while (x < 0); // runs at least onceBreak, 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.
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; }
}
}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.
#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.
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) == 32Funktionsü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.
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 versionLambda-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.
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.
#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); // 12STL-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.
#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.
#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.
#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 >= 3array & 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.
#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, 4tuple & 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.
#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);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.
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); // 2Referenzen 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.
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 nullunique_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.
#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 nullptrshared_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).
#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.
// 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 neededKlassen & 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.
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.
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.
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.
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.
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)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.
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"); // explicitKlassen-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.
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.
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); // specializedVariadische 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.
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.14Concepts (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.
#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; }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.
#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.
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 8accumulate & 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.
#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); // 1copy, 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.
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 3min, 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.
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); // 100Fehlerbehandlung
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.
#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.
#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 messageBenutzerdefinierte 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.
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.
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.
#include <cassert>
double sqrt_safe(double x) {
assert(x >= 0 && "sqrt of negative");
return std::sqrt(x);
}
// In release builds (NDEBUG defined), assert is removedDatei-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.
#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).
#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.
#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).
#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.
#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;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.
#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.
#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 objectweak_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.
#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.
#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 choiceenable_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.
#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 = 2Move-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.
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 rvaluestd::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.
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 emptyPerfect 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.
#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.
// 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.
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
}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.
#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.
#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.
#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.
#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(); // 10Atomare 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.
#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); // releaseTemplate-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.
// 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.
#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.
#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.
#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>.
#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.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.
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, explicitlyGenerische 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.
// 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.
#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.
#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.
#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); // 10Namespaces & 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.
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.
#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.
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.
// 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 codeC++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.
// 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);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.
// 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 TUFunktionsä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++.
// 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.
// # (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).
#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"
#endifVordefinierte 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.
// 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>
#endifDesign 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).
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-safelyRAII (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.
// 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 exceptionPimpl-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.
// 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.
#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::functionCRTP (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.
// 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();
}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.
#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 nullptrshared_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).
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 objectweak_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.
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.
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().
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
}
};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.
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.
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/assignmentPerfect 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.
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.
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.
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
};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.
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.
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.
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 neededConcepts (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.
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.
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
};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).
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.
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)).
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 > 2copy & 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.
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.
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; });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.
#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 destructionstd::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.
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-lockstd::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.
#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.
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.
#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_cstModerne 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.
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.
#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.
#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.
#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, 16Koroutinen (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.
#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 coroutineHä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.
// 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.
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.
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; // UBSlicing
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.
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.
// 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;
}Verwandte C++-Snippets
Copy-paste ready code for common tasks.
Smart Pointers
unique_ptr, shared_ptr, weak_ptr.
RAII
Resource Acquisition Is Initialization.
Move-Semantik
Rvalue-Referenzen und Move-Konstruktoren.
Lambda-Ausdrücke
Lambda und Captures.
Templates
Funktions-Templates und Klassen-Templates.
STL-Container
Häufige Container-Operationen.
STL-Algorithmen
Häufige Algorithmus-Funktionen.
Iterator
Iterator-Typen und Verwendung.
Fehlerbehandlung
try-catch und benutzerdefinierte Exceptions.
Multithreading
thread, mutex, condition_variable.
Datei-IO
Datei-Lese- und Schreiboperationen.
Strings
std::string-Operationen.
Reguläre Ausdrücke
std::regex-Matching und -Ersetzung.
Typ-Deduktion
auto, decltype, Template-Deduktion.
constexpr
Compile-Zeit-Konstanten und -Berechnung.
Was this helpful?