Lambdas & funktionale Programmierung
Meistere C++-Lambdas — Capture-Listen, mutable-Lambdas, generische Lambdas und ihre Verwendung als Algorithmusprädikate. Lerne std::function, std::invoke und das IIFE-Muster.
Lambda-Ausdrücke
Ein Lambda-Ausdruck erzeugt ein anonymes Funktionsobjekt (Closure) inline. Lambdas sind die primäre Methode, um Verhalten an STL-Algorithmen, Callbacks und Event-Handler zu übergeben.
Die vollständige Syntax lautet:
[captures](parameters) mutable -> return_type { body }- Captures — Variablen aus dem umgebenden Gültigkeitsbereich zur Verwendung im Lambda
- Parameters — wie reguläre Funktionsparameter
- mutable — erlaubt die Modifikation von per-Wert-erfassten Variablen
- Return type — wird normalerweise automatisch abgeleitet
- Body — der Funktionskörper
Lambdas werden zu vom Compiler generierten Funktionsobjekten (Structs mit operator()) kompiliert — sie haben keinen Overhead im Vergleich zu handgeschriebenen Funktoren.
Grundlegende Lambda-Syntax und Captures
Die Capture-Liste ist das definierende Merkmal von Lambdas. Sie bestimmt, welche umgebenden Variablen das Lambda verwenden kann und wie.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {1, 5, 3, 8, 2, 9, 4, 7, 6};
// Simplest lambda — no captures
std::sort(nums.begin(), nums.end(),
[](int a, int b) { return a > b; }); // descending
// Capture by value [=] — copies variables
int threshold = 5;
auto above = std::count_if(nums.begin(), nums.end(),
[threshold](int x) { return x > threshold; });
std::cout << "Above " << threshold << ": " << above << '\n';
// Capture by reference [&] — references variables
int sum = 0;
std::for_each(nums.begin(), nums.end(),
[&sum](int x) { sum += x; });
std::cout << "Sum: " << sum << '\n';
// Mixed captures
int min_val = 3, max_val = 7;
auto in_range = std::count_if(nums.begin(), nums.end(),
[min_val, &max_val](int x) { return x >= min_val && x <= max_val; });
// min_val captured by value, max_val captured by reference
// Capture all by value [=] or all by reference [&]
auto print_all = [&]() { // captures everything by reference
for (int n : nums) std::cout << n << ' ';
std::cout << '\n';
};
print_all();
// Init capture (C++14) — create new variables in the capture
auto counter = [count = 0]() mutable { return ++count; };
std::cout << counter() << '\n'; // 1
std::cout << counter() << '\n'; // 2
std::cout << counter() << '\n'; // 3
}Generische und Template-Lambdas
C++14 führte generische Lambdas mit auto-Parametern ein. C++20 erweiterte dies mit expliziten Template-Parameterlisten, die volle Template-Leistung in Lambdas ermöglichen.
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
#include <type_traits>
int main() {
// C++14: generic lambda — auto parameters
auto print = [](const auto& x) {
std::cout << x << '\n';
};
print(42); // int
print(3.14); // double
print("hello"); // const char*
// C++14: generic lambda for comparisons
auto max_of = [](const auto& a, const auto& b) {
return (a > b) ? a : b;
};
std::cout << max_of(3, 7) << '\n'; // 7
std::cout << max_of("abc", "xyz") << '\n'; // xyz (lexicographic)
// C++20: template lambda — explicit template parameters
auto add = []<typename T>(T a, T b) -> T {
return a + b;
};
std::cout << add(1, 2) << '\n'; // 3
std::cout << add(1.5, 2.5) << '\n'; // 4.0
// C++20: constrained template lambda with concepts
auto integral_only = []<std::integral T>(T x) {
return x * 2;
};
std::cout << integral_only(5) << '\n'; // 10
// integral_only(3.14); // ERROR: double doesn't satisfy std::integral
// Using generic lambdas with algorithms
std::vector<std::string> words = {"banana", "apple", "cherry"};
std::sort(words.begin(), words.end(),
[](const auto& a, const auto& b) {
return a.size() < b.size(); // sort by length
});
for (const auto& w : words) std::cout << w << ' '; // apple banana cherry
std::cout << '\n';
}std::function und std::invoke
std::function ist ein typgelöschter Callable-Wrapper, der jedes Callable mit passender Signatur aufnehmen kann — Lambdas, Funktionszeiger, Funktoren, Memberfunktionszeiger. std::invoke ist ein einheitlicher Weg, jedes Callable aufzurufen.
#include <functional>
#include <iostream>
#include <string>
#include <vector>
// Regular function
int add(int a, int b) { return a + b; }
struct Multiplier {
int factor;
int operator()(int x) const { return x * factor; }
int multiply(int x) const { return x * factor; }
};
int main() {
// std::function can hold different callable types
std::function<int(int, int)> op;
op = add; // function pointer
std::cout << op(3, 4) << '\n'; // 7
op = [](int a, int b) { return a * b; }; // lambda
std::cout << op(3, 4) << '\n'; // 12
// Store callables in a collection
std::vector<std::function<int(int)>> transforms;
transforms.push_back([](int x) { return x + 1; });
transforms.push_back([](int x) { return x * 2; });
transforms.push_back(Multiplier{10}); // functor
int val = 5;
for (auto& fn : transforms) {
val = fn(val);
}
std::cout << val << '\n'; // ((5+1)*2)*10 = 120
// std::invoke — uniform call syntax for any callable
Multiplier m{3};
std::cout << std::invoke(add, 10, 20) << '\n'; // 30
std::cout << std::invoke(m, 7) << '\n'; // 21
std::cout << std::invoke(&Multiplier::multiply, m, 7) << '\n'; // 21
std::cout << std::invoke(&Multiplier::factor, m) << '\n'; // 3 (data member)
}Das IIFE-Muster
IIFE (Immediately Invoked Function Expression) ist ein Muster, das aus JavaScript übernommen wurde. Du definierst ein Lambda und rufst es sofort auf. Dies ist nützlich für die komplexe Initialisierung von const-Variablen:
const auto config = [&]() {
Config c;
c.host = read_env("HOST");
c.port = parse_int(read_env("PORT"));
if (c.port <= 0) c.port = 8080;
c.debug = is_debug_build();
return c;
}(); // <-- beachte die () -- sofort aufgerufenOhne IIFE brauchst du entweder eine nicht-const-Variable (Verlust der Unveränderlichkeitsgarantien) oder eine separate Factory-Funktion. IIFE hält die Initialisierungslogik lokal und das Ergebnis const.
Erfassung per Referenz ist gefährlich, wenn das Lambda die erfasste Variable überlebt:
std::function<int()> make_counter() {
int count = 0;
return [&count]() { return ++count; }; // BUG: count wird zerstört!
}Das zurückgegebene Lambda hält eine Referenz auf eine lokale Variable, die nicht mehr existiert. Dies ist undefiniertes Verhalten. Behebe es, indem du per Wert erfasst (mit mutable bei Bedarf):
std::function<int()> make_counter() {
int count = 0;
return [count]() mutable { return ++count; }; // OK: besitzt eine Kopie
}Faustregel: Erfasse per Referenz für kurzlebige Lambdas (Algorithmusprädikate), erfasse per Wert für langlebige Lambdas (Callbacks, in Datenstrukturen gespeichert).
std::function hat Overhead: Es allokiert Speicher (für große Closures), verwendet virtuellen Dispatch und verhindert Inlining. Für Algorithmusprädikate und andere Fälle, in denen das Callable sofort verwendet wird, übergib das Lambda direkt — der Compiler kann es vollständig inlinen.
Verwende std::function nur, wenn du:
- Callables für spätere Verwendung speichern musst
- Verschiedene Callable-Typen in einen einheitlichen Container typgelöscht zusammenfassen musst
- Callables über ABI-Grenzen übergeben musst
In Templates verwende auto oder Concepts, um Callables ohne Typgelöschung zu akzeptieren:
template<std::invocable<int> F>
void apply(F&& fn, int value) {
fn(value); // kein std::function-Overhead
}- Lambdas sind Overhead-freie anonyme Funktionsobjekte — verwende sie uneingeschränkt mit STL-Algorithmen
- Erfasse per Wert
[x]für Sicherheit bei langlebigen Lambdas; erfasse per Referenz[&x]für kurzlebige - C++14 generische Lambdas (
auto-Parameter) und C++20 Template-Lambdas geben volle generische Programmierkraft std::functiontypgelöscht Callables, hat aber Overhead — bevorzugeautooder Templates wenn möglich- Das IIFE-Muster
[&]() { ... }()ermöglicht komplexe Initialisierung vonconst-Variablen - Init-Captures (
[x = expr]) ermöglichen das Erstellen neuer Variablen im Closure (C++14)
Quiz — Teste dein Wissen
(15 XP)1. Was bewirkt das Schlüsselwort `mutable` bei einem Lambda?
2. Welche Gefahr besteht, wenn eine lokale Variable per Referenz in einem Lambda erfasst wird, das von einer Funktion zurückgegeben wird?
3. Was demonstriert `[count = 0]() mutable { return ++count; }`?