Aller au contenu

Lambdas et programmation fonctionnelle

Maîtrisez les lambdas C++ — listes de capture, lambdas mutable, lambdas génériques et leur utilisation comme prédicats d'algorithmes. Apprenez std::function, std::invoke et le patron IIFE.

Expressions lambda

Une expression lambda crée un objet fonction anonyme (closure) en ligne. Les lambdas sont le moyen principal de passer un comportement aux algorithmes STL, aux callbacks et aux gestionnaires d'événements.

La syntaxe complète est :

[captures](parameters) mutable -> return_type { body }

- Captures — variables de la portée englobante à utiliser dans la lambda
- Parameters — comme les paramètres de fonction ordinaires
- mutable — permet de modifier les variables capturées par valeur
- Return type — généralement déduit automatiquement
- Body — le corps de la fonction

Les lambdas sont compilées en objets fonction générés par le compilateur (structs avec operator()) — elles n'ont aucun surcoût par rapport aux foncteurs écrits à la main.

Syntaxe de base des lambdas et captures

La liste de capture est la caractéristique distinctive des lambdas. Elle détermine quelles variables englobantes la lambda peut accéder et comment.

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
}

Lambdas génériques et template

C++14 a introduit les lambdas génériques avec des paramètres auto. C++20 a étendu cela avec des listes de paramètres template explicites, permettant la pleine puissance des templates dans les lambdas.

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 et std::invoke

std::function est un wrapper callable à effacement de type qui peut contenir n'importe quel callable avec une signature correspondante — lambdas, pointeurs de fonction, foncteurs, pointeurs de fonction membre. std::invoke est une façon uniforme d'appeler n'importe quel callable.

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

Le patron IIFE

IIFE (Immediately Invoked Function Expression) est un patron emprunté à JavaScript. Vous définissez une lambda et l'appelez immédiatement. C'est utile pour l'initialisation complexe de variables const :

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;
}();  // <-- notez le () — invoqué immédiatement

Sans IIFE, vous auriez besoin soit d'une variable non-const (perdant les garanties d'immutabilité), soit d'une fonction factory séparée. IIFE garde la logique d'initialisation locale et le résultat const.

Piège

Capturer par référence est dangereux lorsque la lambda survit à la variable capturée :

std::function<int()> make_counter() {
    int count = 0;
    return [&count]() { return ++count; };  // BUG : count est détruit !
}

La lambda retournée contient une référence vers une variable locale qui n'existe plus. C'est un comportement indéfini. Corrigez-le en capturant par valeur (avec mutable si nécessaire) :

std::function<int()> make_counter() {
    int count = 0;
    return [count]() mutable { return ++count; };  // OK : possède une copie
}

Règle générale : capturez par référence pour les lambdas à courte durée de vie (prédicats d'algorithmes), capturez par valeur pour les lambdas à longue durée de vie (callbacks, stockées dans des structures de données).

Bonne pratique

std::function a un surcoût : il alloue de la mémoire (pour les grandes closures), utilise un dispatch virtuel et empêche l'inlining. Pour les prédicats d'algorithmes et les autres cas où le callable est utilisé immédiatement, passez la lambda directement — le compilateur peut l'inliner complètement.

Utilisez std::function uniquement lorsque vous devez :
- Stocker des callables pour une utilisation ultérieure
- Effacer le type de différents types de callable dans un conteneur uniforme
- Passer des callables à travers des frontières ABI

Dans les templates, utilisez auto ou les concepts pour accepter des callables sans effacement de type :

template<std::invocable<int> F>
void apply(F&& fn, int value) {
    fn(value);  // pas de surcoût std::function
}
Points clés
  • Les lambdas sont des objets fonction anonymes sans surcoût — utilisez-les librement avec les algorithmes STL
  • Capturez par valeur [x] pour la sûreté avec les lambdas à longue durée de vie ; capturez par référence [&x] pour les courtes durées
  • Les lambdas génériques C++14 (paramètres auto) et les lambdas template C++20 offrent la pleine puissance de la programmation générique
  • std::function efface le type des callables mais a un surcoût — préférez auto ou les templates quand c'est possible
  • Le patron IIFE [&]() { ... }() permet l'initialisation complexe de variables const
  • Les captures init ([x = expr]) permettent de créer de nouvelles variables dans la closure (C++14)

Quiz — Testez vos connaissances

(15 XP)

1. Que fait le mot-clé `mutable` sur une lambda ?

2. Quel est le danger de capturer une variable locale par référence dans une lambda retournée par une fonction ?

3. Que démontre `[count = 0]() mutable { return ++count; }` ?