Zum Inhalt springen

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.

lambda_captures.cpp
#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.

generic_lambdas.cpp
#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.

std_function.cpp
#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 aufgerufen

Ohne 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.

Stolperfalle

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).

Best Practice

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
}
Wichtigste Erkenntnisse
  • 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::function typgelöscht Callables, hat aber Overhead — bevorzuge auto oder Templates wenn möglich
  • Das IIFE-Muster [&]() { ... }() ermöglicht komplexe Initialisierung von const-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; }`?