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.
#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.
#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.
#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édiatementSans 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.
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).
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
}- 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::functionefface le type des callables mais a un surcoût — préférezautoou les templates quand c'est possible- Le patron IIFE
[&]() { ... }()permet l'initialisation complexe de variablesconst - 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; }` ?