L'essentiel des illustrations de code parallèle en langage natif au sein de Visual Studio 2010, utilise une des nouveautés proposées dans le C++0x: les expressions lambda. Pour ceux qui ne suivent pas régulièrement les évolutions du langage C++, je suis certain que la notation des expressions lambda du C++0x ne vous sont pas familières. Si vous pratiquez la programmation C# 3.0, vous utilisez certainement déjà les expressions lambda. Comme vous le constaterez les expressions lambda du C++0x sont extrêmement puissantes :-).

En C++0x,  les expressions lambda reposent sur des objets de type fonction anonyme. Pour commencer nous nous bornerons à des exemples très simples que nous ferons évoluer progressivement.

using namespace std;

 

int main() {

    vector<int> v;

 

    for (int i = 0; i < 10; ++i) {

        v.push_back(i);

    }

    // Expression Lambda 1

    for_each(v.begin(), v.end(), [](int n) {

         cout << n << " ";

    });

 

    cout << endl;

}

 

Dans le code ci-dessus, nous découvrons une première expression lambda. Celle-ci est introduite avec la syntaxe crochet/crochet,[], ce qui indique au compilateur le début de l’expression. La déclaration du paramètre (int n), fourni au compilateur le paramètre de la pseudo-fonction anonyme. Enfin, le corps de la méthode, { cout << n << " "; }.

 

Par défaut la méthode anonyme ne retourne pas de valeur. Cependant, si l’expression retourne une valeur, le type de retour sera déduit par le compilateur. En revanche, si la déduction n’est pas implicite vous pouvez indiquer le type de retour.

 

    // Expression Lambda 2

    for_each(v.begin(), v.end(), [](int n)->double {

        if (n % 2 == 0) {

        return n * n * n;

        }

        else {

        return n / 2.0;

        }

    });

 

Dans le code ci-dessus, nous introduisons une seconde expression lambda qui précise le type de retour, ->double, afin d'éliminer toute ambiguïté pour le compilateur.

Jusqu’ici nous avons introduit des expressions lambda sans état, cependant il est parfaitement possible de capturer les valeurs courantes de la pile où se trouve plongé l’expression. Si la partie préface de l'expression lambda est vide, [], on dit que l’expression lambda est sans état. Mais il est aussi parfaitement correct de spécifier les éléments capturés.

    int x = 2;

    double y = 2.0;

 

    // Expression Lambda 3

    for_each(v.begin(), v.end(), [x, y](int n)->double {

        if (n % x == 0) {

        return n * n * n;

        }

        else {

        return n / y;

        }

    });

 

Dans le code ci-dessus, nous apportons à notre expression lambda deux paramètres, [x, y], permettant de capturer depuis la pile les variables x et y. Cependant il est aussi possible de capturer toutes les variables de la pile courante par valeur.

    int x = 2;

    double y = 2.0;

 

    // Expression Lambda 4

    for_each(v.begin(), v.end(), [=](int n)->double {

        if (n % x == 0) {

        return n * n * n;

        }

        else {

        return n / y;

        }

    });

 

Dans le code ci-dessus, nous apportons à notre expression lambda une capture complète de la pile via la syntaxe [=].

Par défaut une méthode lambda est déclarée comme const. Si vous souhaiter modifier la valeur des paramètres dans le corps de l’expression vous aurez recours au mot clef mutable.

// Expression Lambda 5

    for_each(v.begin(), v.end(), [x, y](int &n) mutable {

        const int old = n;

        n *= x * y;

        x = y;

        y = old;   

    });

 

Dans le code ci-dessus, nous apportons à notre expression lambda la capture de variable par référence via le symbole & (int &n).

Cependant, il est aussi possible d’éviter de passer des éléments capturés par copie en utilisant des références avec quelques variables passées par référence ce qui nous permet de nous passer du mot clef mutable.

// Expression Lambda 6

for_each(v.begin(), v.end(), [&x, &y](int &n) {

        const int old = n;

        n *= x * y;

        x = y;

        y = old;   

    });

 

Enfin, il est aussi possible d’exprimer une capture de toutes les variables de la pile courante par référence en utilisant la syntaxe [&]. Dans l'exemple ci-dessous, nous illustrons la méthode parallel_for permettant de paralléliser une collection de données très facilement en capturant toute la pile par référence.

   // Expression Lambda 7

   parallel_for(0, size, 1, [&](int i) {

        for (int j = 0; j < size; j++) {

            double temp = 0;

            for (int k = 0; k < size; k++) {

                temp += m1[i][k] * m2[k][j];

            }

            result[i][j] = temp;

        }

   });

 

En résumé, nous pouvons confirmer que les expressions lambda du C++0x sont à la fois souples et très puissantes respectant la philosophie du C++. Dans mes prochains billets j'illustrerai régulièrement les librairies de l'offre parallèle C++ de Visual Studio qui utilisent largement les expressions lambda. J'espère que cette petite introduction vous permettra de comprendre aisément les exemples de code :-)

A bientôt,

Bruno

Bruno Boucard (boucard.bruno@free.fr)