Démystifions les lambda expressions de C++ avec cppinsights
Tags: cpp
Published: 2020-05-26 13:13:56

Les _lambdas expressions_ sont apparues en C++11 et sont une des _features_ qui font vraiment la différence entre le C++ à l'ancienne et le C++ moderne. Il y a des tonnes de bons articles sur internet qui expliquent leurs syntaxes et donnent des bonnes pratiques pour les utiliser. La référence reste [cppreference](https://en.cppreference.com/w/cpp/language/lambda) même si ce n'est pas forcément la ressource la plus simple pour découvrir les _lambdas expressions_.

L'objectif de cet article est de comprendre ce que le compilateur fait quand on utilise des _lambdas expressions_ et ainsi démystifier ce qui ressemble de prime abord à de la magie (mais comme vous le savez tous, il n'y pas de magie dans la vraie vie).

C'est aussi l'occasion de vous présenter [cppinsights](https://cppinsights.io/), un copain de [Compiler Explorer](https://godbolt.org/) dont je vous ai parlé [en vidéo en décembre](https://www.younup.fr/blog/toi-aussi-decompile-ton-code-avec-compiler-explorer). Quand je dis qu'ils sont copains, je n'invente rien : chacun possède un bouton pour ouvrir le code dans l'autre.

## cppinsights

Le principe de [cppinsights](https://cppinsights.io/) est très semblable à celui de Compiler Explorer : à gauche vous taper du code et à droite il vous montre des trucs. Ces trucs, c'est votre code "modifié" pour vous montrer ce qu'il fait vraiment. Voici un exemple avec un [_range-based for loop_](https://en.cppreference.com/w/cpp/language/range-for) :

```cpp
#include <array>
#include <iostream>

int main()
{
    std::array array{1, 2, 3, 4, 5};

    for (const auto &e : array)
    {
        std::cout << e << '\n';
    }
}
```

On obtient :

```cpp
#include <array>
#include <iostream>

int main()
{
    std::array<int, 5> array = {{1, 2, 3, 4, 5}};
    {
    std::array<int, 5> & __range1 = array;
    int * __begin1 = __range1.begin();
    int * __end1 = __range1.end();
    for(; __begin1 != __end1; ++__begin1)
    {
        const int & e = *__begin1;
        std::operator<<(std::cout.operator<<(e), '\n');
    }
    }
}
```

On voit la déduction automatique du type réel de l'objet `array`, on voit que la boucle a été remplacée par un simple parcours d'itérateur, on voit que le chaînage des `<<` a été remplacé par des appels de fonctions imbriqués.

C'est ça cppinsights : sans aller à l'assembleur, mieux comprendre ce que fait votre code.

## Le but d'une lambda expression

Reprenons la définition d'une _lambda expression_ donnée par cppreference :

> Constructs a [closure](https://en.wikipedia.org/wiki/Closure_(computer_programming) ): an unnamed function object capable of capturing variables in scope.

En suivant le lien Wikipédia [_closure_](https://en.wikipedia.org/wiki/Closure_(computer_programming), on lit cette phrase intéressante dans l'introduction :

> Operationally, a closure is a record storing a function together with an environment.

Une _lambda expression_ va donc générer une fonction (qu'on pourra appeler, c'est le but) en stockant un environnement (c'est la notion de capture).

Quand on réfléchit instinctivement à comment on ferait si on n'avait pas de support des lambdas dans le langage, on se dit qu'il faudrait réussir à :

1.  créer un objet avec un opérateur `operator()` (qu'on appelle _function call operator_) et qui serait donc un [_function object_](https://en.cppreference.com/w/cpp/named_req/FunctionObject).
2.  stocker les données à capturer (si données il y a).

En fait, c'est exactement comme ça que ça marche.

## Les lambdas dans la moulinette de cppinsights

Passer du code avec des lambdas dans cppinsights nous permettra de voir le code que le compilateur génère pour faire fonctionner tout ça.

Il y a 2 points qu'on peut traiter séparément :

*   la génération du _function call operator_
*   la capture du contexte



### Appel à la lambda

Pour simplifier, on fera ici des lambdas qui ne capturent rien.

#### Pas de paramètres

On est dans le cas le plus simple d'une _lambda expression_ : on ne capture rien et on l'appelle sans lui passer de paramètre.

```cpp
#include <iostream>

int main()
{
    auto print = []() {
        std::cout << 42 << '\n';
    };
    print();
}
```

Résultat avec cppinsights :

```cpp
#include <iostream>

int main()
{

    class __lambda_5_14
    {
    public: 
    inline /*constexpr */ void operator()() const
    {
        std::operator<<(std::cout.operator<<(42), '\n');
    }

    using retType_5_14 = void (*)();
    inline /*constexpr */ operator retType_5_14 () const noexcept
    {
        return __invoke;
    };

    private: 
    static inline void __invoke()
    {
        std::operator<<(std::cout.operator<<(42), '\n');
    }

    };

    __lambda_5_14 print = __lambda_5_14{};
    print.operator()();
}
```

C'est en fait tout simple : la lambda génère une classe avec un _function call operator_ sans paramètre, en crée une instance et appelle l'opérateur sur cette instance.

C'est le cas le plus simple et pourtant il est déjà super instructif puisqu'on voit la technique utilisée : créer une classe à la volée. Une _closure_ est donc une classe.

On voit aussi pourquoi on dit souvent que "seul le compilateur connait le type réel de la lambda". Effectivement, le compilateur génère un nom que vous ne pouvez pas vraiment deviner. C'est pour ça qu'on utilise toujours `auto` pour stocker le résultat d'une _lambda expression_.

Il y aussi un opérateur pour convertir la _closure_ en pointeur sur fonction (qui n'est pas utilisé ici). Ce type de conversion n'est possible qu'avec les lambdas non capturantes. Vous verrez dans les sections suivantes que cet opérateur n'apparait plus. On peut lire sur [cppreference](https://en.cppreference.com/w/cpp/language/lambda) :

> `ClosureType::operator ret(*)(params)()`
> 
> This user-defined conversion function is only defined **if the capture list** of the lambda-expression **is empty**. It is a public, constexpr (since C++17), non-virtual, non-explicit, const noexcept (since C++14) member function of the closure object.

#### Avec des paramètres

Ajoutons simplement un paramètre à l'appel de la lambda :

```cpp
#include <iostream>

int main()
{
    auto print = [](int value) {
        std::cout << value << '\n';
    };
    print(42);
}
```

Résultat avec cppinsights :

```cpp
#include <iostream>

int main()
{

    class __lambda_5_18
    {
    public: 
    inline /*constexpr */ void operator()(int value) const
    {
        std::operator<<(std::cout.operator<<(value), '\n');
    }

    using retType_5_18 = void (*)(int);
    inline /*constexpr */ operator retType_5_18 () const noexcept
    {
        return __invoke;
    };

    private: 
    static inline void __invoke(int value)
    {
        std::operator<<(std::cout.operator<<(value), '\n');
    }

    };

    __lambda_5_18 print = __lambda_5_18{};
    print.operator()(42);
}
```

Rien de bien incroyable ici. Le _function call operator_ a maintenant un paramètre et l'opérateur de conversion en pointeur sur fonction a été adapté en conséquence.

#### Avec une valeur de retour

Je ne pense pas avoir vraiment besoin de vous montrez ça mais puisque je suis lancé :

```cpp
int main()
{
    auto square = [](int a) {
        return a * a;
    };

    return square(42);
}
```

Résultat avec cppinsights :

```cpp
int main()
{

    class __lambda_3_19
    {
    public: 
    inline /*constexpr */ int operator()(int a) const
    {
        return a * a;
    }

    using retType_3_19 = int (*)(int);
    inline /*constexpr */ operator retType_3_19 () const noexcept
    {
        return __invoke;
    };

    private: 
    static inline int __invoke(int a)
    {
        return a * a;
    }

    };

    __lambda_3_19 square = __lambda_3_19{};
    return square.operator()(42);
}
```

Dans le mille : l'opérateur `operator()` a cette fois un type de retour et fait un `return`.

### Capture de contexte

On peut faire plein de choses intéressantes avec les lambdas non capturantes mais la puissance s'exprime pleinement quand on capture du contexte. Il y a 2 techniques de capture : par copie ou par référence. Une lambda peut utiliser les 2 techniques à la fois pour capturer son contexte de manière précise.

Prenons le même bout de code, avec une lambda qui capture une variable, et voyons les conséquences de la méthode de capture. Essayons d'abord par copie puis par référence.

#### Capture par copie

```cpp
#include <iostream>

int main()
{
    int value = 42;

    auto print = [value]() {
        std::cout << value << '\n';
    };
    print();
}
```

Résultat avec cppinsights :

```cpp
#include <iostream>

int main()
{
    int value = 42;

    class __lambda_7_18
    {
    public: 
    inline /*constexpr */ void operator()() const
    {
        std::operator<<(std::cout.operator<<(value), '\n');
    }

    private: 
    int value;

    public:
    __lambda_7_18(int _value)
    : value{_value}
    {}

    };

    __lambda_7_18 print = __lambda_7_18{value};
    print.operator()();
}
```

On commence à voir des choses intéressantes, des choses qui démystifient les lambdas et leur capture de contexte. La classe générée a un constructeur et des champs pour stocker tout ce qui est capturé. C'est aussi simple que ça.

On constate bien qu'il n'y a pas d'opérateur de conversion en pointeur sur cette fonction cette fois.

#### Capture par référence

Vous vous doutez très logiquement de ce qui va se passer lorsqu'on va capturer par référence plutôt que par copie :

```cpp
#include <iostream>

int main()
{
    int value = 42;

    auto print = [&value]() {
        std::cout << value << '\n';
    };
    print();
}
```

Résultat avec cppinsights :

```cpp
#include <iostream>

int main()
{
    int value = 42;

    class __lambda_7_18
    {
    public: 
    inline /*constexpr */ void operator()() const
    {
        std::operator<<(std::cout.operator<<(value), '\n');
    }

    private: 
    int & value;

    public:
    __lambda_7_18(int & _value)
    : value{_value}
    {}

    };

    __lambda_7_18 print = __lambda_7_18{value};
    print.operator()();
}
```

Le constructeur prend cette fois en paramètre une référence et le champ privé permet de converser cette référence.

#### Capture par référence constante

Puisqu'on y est, parlons de la capture par référence constante. Ben oui vous avez vu que la référence dans le code "généré" ci-dessus n'est pas `const` alors qu'on ne modifie pas l'objet référencé.

Ce n'est pas possible avec le simple ajout du mot `const` dans la liste de capture. Depuis C++17, il y a [une astuce assez simple](https://stackoverflow.com/questions/3772867/lambda-capture-as-const-reference) :

```cpp
#include <iostream>
#include <utility>

int main()
{
    int value = 42;

    auto print = [&value = std::as_const(value)]() {
        std::cout << value << '\n';
    };
    print();
}
```

Résultat avec cppinsights :

```cpp
#include <iostream>
#include <utility>

int main()
{
    int value = 42;

    class __lambda_8_18
    {
    public: 
    inline /*constexpr */ void operator()() const
    {
        std::operator<<(std::cout.operator<<(value), '\n');
    }

    private: 
    const int & value;

    public:
    __lambda_8_18(const int & _value)
    : value{_value}
    {}

    };

    __lambda_8_18 print = __lambda_8_18{std::as_const(value)};
    print.operator()();
}
```

Et voilà ! Impeccable !

## Retourner une lambda depuis une fonction

Il est possible de renvoyer une _closure_ depuis une fonction. On rentre alors dans le vaste sujet des `std::functions` et de leur gestion en mémoire, et on sort ainsi du cadre de cet article. Vous pouvez lire [cette discussion stackoverflow](https://stackoverflow.com/questions/12202656/c11-lambda-implementation-and-memory-model) sur le sujet pour en savoir un peu plus.

Ce qu'il est très important de savoir est qu'il y a [un risque de _dangling reference_](https://en.cppreference.com/w/cpp/language/lambda) :

> If a non-reference entity is captured by reference, implicitly or explicitly, and the function call operator of the closure object is invoked after the entity's lifetime has ended, undefined behavior occurs. The C++ closures do not extend the lifetimes of the captured references.
> 
> Same applies to the lifetime of the object pointed to by the captured this pointer.

Ce qui a été expliqué plus haut vous permet de comprendre pourquoi : quand on capture par référence, la _closure_ référence des éléments qui ont potentiellement été créés sur la pile de la fonction où la _lambda expression_ a été faite. Quand cette fonction va se terminer en retournant la _closure_, la pile va changer et la _closure_ pointera alors sur une zone mémoire qui est devenue invalide. C'est le principe même d'une [_dangling reference_](https://en.cppreference.com/w/cpp/language/reference).

Le problème n’apparaît bien sûr pas quand on copie le contexte. La _closure_ est alors "autonome".

## Pour en finir

Maintenant, vous cernez normalement mieux comment une _lambda expression_ permet de générer une _closure_, cette classe qui stocke les éléments capturés et a un opérateur `operator()`, ce qui permet "d'appeler la lambda".

En creux, vous aurez sans doute deviné pourquoi on dit de ne pas utiliser les modes de capture par défaut `[&]` et `[=]`. La classe générée possède alors un champ par variable visible dans le _scope_ courant, potentiellement en faisant des copies de tout le monde. Même si ça semble pratique, ce n'est peut-être pas du tout ce que vous voulez : ça peut impacter les performances, ça peut ne pas compiler si l'une des classes n'est pas copiable, ça peut ne plus compiler quand on modifie une des classes pour la rendre non copiable (alors que ça se passait très bien avant), en plus de capturer des choses dont vous n'avez pas besoin et que vous pourriez modifier par erreur (puisque, par défaut, les références ne sont pas constantes).

Les _lambda expressions_ sont vraiment un outil puissant pour écrire du C++ moderne. Prenez le temps d'expérimenter, de comprendre comment les utiliser, et votre code n'en sera que meilleur !

______
Cet article a été [initialement publié](https://www.younup.fr/blog/demystifions-les-lambda-expressions-de-c-avec-cppinsight) sur le blog de Younup : https://www.younup.fr/blog