Recherche…


Syntaxe

  • [ default-capture , capture-list ] ( liste des arguments ) attributs de spécification de jetable mutable -> type de retour { lambda-body } // Ordre des spécificateurs et attributs lambda.
  • [ capture-list ] ( liste d' arguments ) { lambda-body } // Définition lambda commune.
  • [=] ( liste d'arguments ) { lambda-body } // Capture toutes les variables locales nécessaires par valeur.
  • [&] ( liste d'arguments ) { lambda-body } // Capture toutes les variables locales nécessaires par référence.
  • [ capture-list ] { lambda-body } // La liste des arguments et les spécificateurs peuvent être omis.

Paramètres

Paramètre Détails
capture par défaut Spécifie comment toutes les variables non répertoriées sont capturées. Peut être = (capture par valeur) ou & (capture par référence). En cas d'omission, les variables non répertoriées sont inaccessibles dans le corps lambda . La capture par défaut doit précéder la liste de capture .
liste de capture Spécifie comment les variables locales sont rendues accessibles dans le corps lambda . Les variables sans préfixe sont capturées par valeur. Les variables précédées de & sont capturées par référence. Dans une méthode de classe, this peut être utilisé pour rendre tous ses membres accessibles par référence. Les variables non répertoriées sont inaccessibles, sauf si la liste est précédée d'une capture par défaut .
liste d'arguments Spécifie les arguments de la fonction lambda.
mutable (facultatif) Normalement, les variables capturées par valeur sont const . Spécifier le mutable rend non const. Les modifications apportées à ces variables sont conservées entre les appels.
spécification jet (facultatif) Spécifie le comportement de lancement d'exception de la fonction lambda. Par exemple: noexcept ou throw(std::exception) .
les attributs (facultatif) Tout attribut pour la fonction lambda. Par exemple, si le corps lambda génère toujours une exception, alors [[noreturn]] peut être utilisé.
-> type de retour (facultatif) Spécifie le type de retour de la fonction lambda. Obligatoire lorsque le type de retour ne peut pas être déterminé par le compilateur.
corps lambda Un bloc de code contenant l'implémentation de la fonction lambda.

Remarques

C ++ 17 (le brouillon actuel) introduit constexpr lambdas, essentiellement des lambdas qui peuvent être évalués au moment de la compilation. Un lambda est automatiquement constexpr s'il satisfait constexpr exigences de constexpr , mais vous pouvez également le spécifier à l'aide du mot clé constexpr :

//Explicitly define this lambdas as constexpr
[]() constexpr {
    //Do stuff
}

Qu'est-ce qu'une expression lambda?

Une expression lambda fournit un moyen concis de créer des objets de fonction simples. Une expression lambda est une valeur dont l'objet de résultat est appelé objet de fermeture , qui se comporte comme un objet fonction.

Le nom «expression lambda» provient du lambda calculus , un formalisme mathématique inventé dans les années 1930 par Alonzo Church pour étudier les questions de logique et de calculabilité. Le Lambda calcul est à la base du langage LISP , un langage de programmation fonctionnel. Par rapport au lambda calcul et au LISP, les expressions lambda en C ++ partagent les propriétés d'être non nommé et capturent les variables du contexte environnant, mais elles n'ont pas la capacité d'opérer et de renvoyer des fonctions.

Une expression lambda est souvent utilisée comme argument pour les fonctions qui prennent un objet appelable. Cela peut être plus simple que de créer une fonction nommée, qui ne serait utilisée qu’en tant qu’argument. Dans de tels cas, les expressions lambda sont généralement préférées car elles permettent de définir les objets de fonction en ligne.

Un lambda se compose généralement de trois parties: une liste de capture [] , une liste de paramètres facultative () et un corps {} , tous pouvant être vides:

[](){}                // An empty lambda, which does and returns nothing

Liste de capture

[] est la liste de capture . Par défaut, les variables de la portée englobante ne sont pas accessibles par un lambda. La capture d' une variable la rend accessible à l'intérieur du lambda, soit comme copie, soit comme référence . Les variables capturées font partie du lambda; contrairement aux arguments de fonction, ils ne doivent pas être transmis lors de l'appel du lambda.

int a = 0;                       // Define an integer variable
auto f = []()   { return a*9; }; // Error: 'a' cannot be accessed
auto f = [a]()  { return a*9; }; // OK, 'a' is "captured" by value
auto f = [&a]() { return a++; }; // OK, 'a' is "captured" by reference
                                 //      Note: It is the responsibility of the programmer
                                 //      to ensure that a is not destroyed before the
                                 //      lambda is called.
auto b = f();                    // Call the lambda function. a is taken from the capture list and not passed here.

Liste de paramètres

() est la liste de paramètres , qui est presque la même que dans les fonctions normales. Si le lambda ne prend aucun argument, ces parenthèses peuvent être omises (sauf si vous devez déclarer le mutable lambda). Ces deux lambda sont équivalentes:

auto call_foo  = [x](){ x.foo(); };
auto call_foo2 = [x]{ x.foo(); };
C ++ 14

La liste de paramètres peut utiliser le type d'espace réservé auto au lieu des types réels. Ce faisant, cet argument se comporte comme un paramètre de modèle d'un modèle de fonction. Les lambda suivants sont équivalents lorsque vous voulez trier un vecteur en code générique:

auto sort_cpp11 = [](std::vector<T>::const_reference lhs, std::vector<T>::const_reference rhs) { return lhs < rhs; }; 
auto sort_cpp14 = [](const auto &lhs, const auto &rhs) { return lhs < rhs; }; 

Corps de fonction

{} est le corps , qui est le même que dans les fonctions régulières.


Appeler un lambda

L'objet résultat d'une expression lambda est une fermeture , qui peut être appelée à l'aide de l' operator() (comme pour les autres objets de fonction):

int multiplier = 5;
auto timesFive = [multiplier](int a) { return a * multiplier; }; 
std::out << timesFive(2); // Prints 10

multiplier = 15;
std::out << timesFive(2); // Still prints 2*5 == 10

Type de retour

Par défaut, le type de retour d'une expression lambda est déduit.

[](){ return true; };

Dans ce cas, le type de retour est bool .

Vous pouvez également spécifier manuellement le type de retour en utilisant la syntaxe suivante:

[]() -> bool { return true; };

Lambda Mutable

Les objets capturés par valeur dans le lambda sont par défaut immuables. C'est parce que l' operator() de l'objet de fermeture généré est const par défaut.

auto func = [c = 0](){++c; std::cout << c;};  // fails to compile because ++c
                                              // tries to mutate the state of
                                              // the lambda.

La modification peut être autorisée en utilisant le mot-clé mutable , qui rend l' operator() non- const l'objet plus proche:

auto func = [c = 0]() mutable {++c; std::cout << c;};

Si utilisé avec le type de retour, le mutable vient avant lui.

auto func = [c = 0]() mutable -> int {++c; std::cout << c; return c;};

Un exemple pour illustrer l'utilité de lambdas

Avant C ++ 11:

C ++ 11
// Generic functor used for comparison
struct islessthan
{
    islessthan(int threshold) : _threshold(threshold) {}

    bool operator()(int value) const
    {
        return value < _threshold;
    }
private:
    int _threshold;
};

// Declare a vector
const int arr[] = { 1, 2, 3, 4, 5 };
std::vector<int> vec(arr, arr+5);

// Find a number that's less than a given input (assume this would have been function input)
int threshold = 10;
std::vector<int>::iterator it = std::find_if(vec.begin(), vec.end(), islessthan(threshold));

Depuis C ++ 11:

C ++ 11
// Declare a vector
std::vector<int> vec{ 1, 2, 3, 4, 5 };

// Find a number that's less than a given input (assume this would have been function input)
int threshold = 10;
auto it = std::find_if(vec.begin(), vec.end(), [threshold](int value) { return value < threshold; });

Spécification du type de retour

Pour les lambdas avec une seule instruction de retour ou plusieurs instructions de retour dont les expressions sont du même type, le compilateur peut en déduire le type de retour:

// Returns bool, because "value > 10" is a comparison which yields a Boolean result
auto l = [](int value) {
    return value > 10;
}

Pour les lambdas avec plusieurs instructions de retour de différents types, le compilateur ne peut pas déduire le type de retour:

// error: return types must match if lambda has unspecified return type
auto l = [](int value) {
    if (value < 10) {
        return 1;
    } else {
        return 1.5;
    }
};

Dans ce cas, vous devez spécifier le type de retour explicitement:

// The return type is specified explicitly as 'double'
auto l = [](int value) -> double {
    if (value < 10) {
        return 1;
    } else {
        return 1.5;
    }
};

Les règles pour cela correspondent aux règles de déduction auto . Les Lambdas sans types de retour explicitement spécifiés ne renvoient jamais de références. Par conséquent, si un type de référence est souhaité, il doit également être spécifié explicitement:

auto copy = [](X& x) { return x; };       // 'copy' returns an X, so copies its input
auto ref  = [](X& x) -> X& { return x; }; // 'ref' returns an X&, no copy

Capturer par valeur

Si vous spécifiez le nom de la variable dans la liste de capture, le lambda le capturera par valeur. Cela signifie que le type de fermeture généré pour le lambda stocke une copie de la variable. Cela nécessite également que le type de la variable soit constructible par copie :

int a = 0;

[a]() {
    return a;   // Ok, 'a' is captured by value
};
C ++ 14
auto p = std::unique_ptr<T>(...);

[p]() {         // Compile error; `unique_ptr` is not copy-constructible
    return p->createWidget(); 
};

A partir de C ++ 14 on peut initialiser des variables sur place. Cela permet aux types de déplacement uniquement d'être capturés dans le lambda.

C ++ 14
auto p = std::make_unique<T>(...);

[p = std::move(p)]() {
    return p->createWidget(); 
};

Même si un lambda capture des variables par valeur quand elles sont données par leur nom, ces variables ne peuvent pas être modifiées par défaut dans le corps lambda. En effet, le type de fermeture place le corps lambda dans une déclaration d' operator() const .

Le const s'applique aux accès aux variables membres du type de fermeture et aux variables capturées qui sont membres de la fermeture (toutes les apparences contraires):

int a = 0;

[a]() {
    a = 2;      // Illegal, 'a' is accessed via `const`

    decltype(a) a1 = 1; 
    a1 = 2; // valid: variable 'a1' is not const
};

Pour supprimer le const , vous devez spécifier le mot-clé mutable sur le lambda:

int a = 0;

[a]() mutable {
    a = 2;      // OK, 'a' can be modified
    return a;
};

Comme a été capturé par valeur, toute modification effectuée en appelant le lambda n'affectera pas a . La valeur de a été copié dans le lambda quand il a été construit, de sorte que la copie de la lambda a est séparée de l'extérieur d' a variable.

int a = 5 ; 
auto plus5Val = [a] (void) { return a + 5 ; } ; 
auto plus5Ref = [&a] (void) {return a + 5 ; } ; 

a = 7 ; 
std::cout << a << ", value " << plus5Val() << ", reference " << plus5Ref() ;
// The result will be "7, value 10, reference 12"

Capture généralisée

C ++ 14

Les Lambdas peuvent capturer des expressions, plutôt que de simples variables. Cela permet aux lambdas de stocker les types de déplacement uniquement:

auto p = std::make_unique<T>(...);

auto lamb = [p = std::move(p)]() //Overrides capture-by-value of `p`.
{
  p->SomeFunc();
};

Cela déplace la variable externe p dans la variable de capture lambda, également appelée p . lamb possède maintenant la mémoire allouée par make_unique . Parce que la fermeture contient un type non copiable, cela signifie que l' lamb est lui-même non copiable. Mais il peut être déplacé:

auto lamb_copy = lamb; //Illegal
auto lamb_move = std::move(lamb); //legal.

Maintenant, lamb_move possède la mémoire.


Notez que std::function<> nécessite que les valeurs stockées soient copiables. Vous pouvez écrire votre propre std::function nécessitant un déplacement , ou vous pouvez simplement insérer le lambda dans un wrapper shared_ptr :

auto shared_lambda = [](auto&& f){
  return [spf = std::make_shared<std::decay_t<decltype(f)>>(decltype(f)(f))]
  (auto&&...args)->decltype(auto) {
    return (*spf)(decltype(args)(args)...);
  };
};
auto lamb_shared = shared_lambda(std::move(lamb_move));

prend notre mouvement seul lambda et place son état dans un pointeur partagé puis retourne un lambda qui peut être copié, puis stocké dans une std::function ou similaire.


La capture généralisée utilise auto déduction auto pour le type de la variable. Il déclarera ces captures comme valeurs par défaut, mais elles peuvent également être des références:

int a = 0;

auto lamb = [&v = a](int add) //Note that `a` and `v` have different names
{
  v += add; //Modifies `a`
};

lamb(20); //`a` becomes 20.

La capture de généralisation n'a pas besoin de capturer une variable externe. Il peut capturer une expression arbitraire:

auto lamb = [p = std::make_unique<T>(...)]()
{
    p->SomeFunc();
}

Ceci est utile pour donner aux valeurs lambdas arbitraires qu'elles peuvent contenir et éventuellement modifier, sans avoir à les déclarer en externe à la lambda. Bien entendu, cela n'est utile que si vous n'avez pas l'intention d'accéder à ces variables une fois que le lambda a terminé son travail.

Capturer par référence

Si vous faites précéder le nom d'une variable locale par un & , la variable sera capturée par référence. Conceptuellement, cela signifie que le type de fermeture de lambda aura une variable de référence, initialisée en référence à la variable correspondante en dehors de la portée de lambda. Toute utilisation de la variable dans le corps lambda fera référence à la variable d'origine:

// Declare variable 'a'
int a = 0;

// Declare a lambda which captures 'a' by reference
auto set = [&a]() {
    a = 1;
};

set();
assert(a == 1);

Le mot clé mutable n'est pas nécessaire car a lui-même n'est pas const .

Bien sûr, capturer par référence signifie que le lambda ne doit pas échapper à la portée des variables qu'il capture. Vous pouvez donc appeler des fonctions qui prennent une fonction, mais vous ne devez pas appeler une fonction qui stockera le lambda au-delà de la portée de vos références. Et vous ne devez pas retourner le lambda.

Capture par défaut

Par défaut, les variables locales qui ne sont pas explicitement spécifiées dans la liste de capture ne sont pas accessibles depuis le corps lambda. Cependant, il est possible de capturer implicitement les variables nommées par le corps lambda:

int a = 1;
int b = 2;

// Default capture by value
[=]() { return a + b; }; // OK; a and b are captured by value

// Default capture by reference
[&]() { return a + b; }; // OK; a and b are captured by reference

La capture explicite peut toujours être effectuée parallèlement à la capture implicite par défaut. La définition de capture explicite remplacera la capture par défaut:

int a = 0;
int b = 1;

[=, &b]() {
    a = 2; // Illegal; 'a' is capture by value, and lambda is not 'mutable'
    b = 2; // OK; 'b' is captured by reference
};

Lambdas génériques

c ++ 14

Les fonctions Lambda peuvent prendre des arguments de types arbitraires. Cela permet à un lambda d'être plus générique:

auto twice = [](auto x){ return x+x; };

int i = twice(2); // i == 4
std::string s = twice("hello"); // s == "hellohello"

Ceci est implémenté en C ++ en faisant en sorte que l' operator() du type de fermeture surcharge une fonction de modèle. Le type suivant a un comportement équivalent à la fermeture lambda ci-dessus:

struct _unique_lambda_type
{
  template<typename T>
  auto operator() (T x) const {return x + x;}
};

Tous les paramètres d'un lambda générique n'ont pas besoin d'être génériques:

[](auto x, int y) {return x + y;}

Ici, x est déduit en fonction du premier argument de la fonction, alors que y sera toujours int .

Les lambdas génériques peuvent également prendre des arguments en utilisant les règles habituelles pour auto et & . Si un paramètre générique est considéré comme auto&& , c'est une référence de transfert au passé dans l' argumentation et non une référence rvalue :

auto lamb1 = [](int &&x) {return x + 5;};
auto lamb2 = [](auto &&x) {return x + 5;};
int x = 10;
lamb1(x); // Illegal; must use `std::move(x)` for `int&&` parameters.
lamb2(x); // Legal; the type of `x` is deduced as `int&`.

Les fonctions Lambda peuvent être variadiques et transmettre parfaitement leurs arguments:

auto lam = [](auto&&... args){return f(std::forward<decltype(args)>(args)...);};

ou:

auto lam = [](auto&&... args){return f(decltype(args)(args)...);};

qui ne fonctionne "correctement" qu'avec des variables de type auto&& .

Une bonne raison d'utiliser des lambdas génériques est la syntaxe de visite.

boost::variant<int, double> value;
apply_visitor(value, [&](auto&& e){
  std::cout << e;
});

Ici, nous visitons de manière polymorphe; mais dans d'autres contextes, les noms du type que nous passons ne sont pas intéressants:

mutex_wrapped<std::ostream&> os = std::cout;
os.write([&](auto&& os){
  os << "hello world\n";
});

Répéter le type de std::ostream& est ici du bruit; ce serait comme devoir mentionner le type d'une variable chaque fois que vous l'utilisez. Ici, nous créons un visiteur, mais pas un visiteur polymorphe; auto est utilisé pour la même raison que vous pourriez utiliser auto dans une boucle for(:) .

Conversion en pointeur de fonction

Si la liste de capture d'un lambda est vide, le lambda a une conversion implicite en un pointeur de fonction qui prend les mêmes arguments et retourne le même type de retour:

auto sorter = [](int lhs, int rhs) -> bool {return lhs < rhs;};

using func_ptr = bool(*)(int, int);
func_ptr sorter_func = sorter; // implicit conversion

Une telle conversion peut également être effectuée en utilisant un opérateur unaire plus:

func_ptr sorter_func2 = +sorter; // enforce implicit conversion

L'appel de ce pointeur de fonction se comporte exactement comme l'appel de l' operator() sur le lambda. Ce pointeur de fonction ne dépend en aucun cas de l'existence de la fermeture lambda source. Il peut donc survivre à la fermeture de lambda.

Cette fonctionnalité est principalement utile pour utiliser des lambdas avec des API qui traitent des pointeurs de fonction, plutôt que des objets de fonction C ++.

C ++ 14

La conversion en un pointeur de fonction est également possible pour les lambda génériques avec une liste de capture vide. Si nécessaire, la déduction des arguments du modèle sera utilisée pour sélectionner la spécialisation appropriée.

auto sorter = [](auto lhs, auto rhs) { return lhs < rhs; };
using func_ptr = bool(*)(int, int);
func_ptr sorter_func = sorter;  // deduces int, int
// note however that the following is ambiguous
// func_ptr sorter_func2 = +sorter;

Classe lambda et capture de cette

Une expression lambda évaluée dans une fonction membre d'une classe est implicitement un ami de cette classe:

class Foo
{
private:
    int i;
    
public:
    Foo(int val) : i(val) {}
    
    // definition of a member function
    void Test()
    {
        auto lamb = [](Foo &foo, int val)
        {
            // modification of a private member variable
            foo.i = val;
        };
        
        // lamb is allowed to access a private member, because it is a friend of Foo
        lamb(*this, 30);
    }
};

Un tel lambda n'est pas seulement un ami de cette classe, il a le même accès que la classe dans laquelle il est déclaré.

Lambdas peut capturer le pointeur this qui représente l'instance d'objet sur laquelle la fonction externe a été appelée. Cela se fait en ajoutant this à la liste de capture:

class Foo
{
private:
    int i;
    
public:
    Foo(int val) : i(val) {}
    
    void Test()
    {
        // capture the this pointer by value
        auto lamb = [this](int val)
        {
            i = val;
        };
        
        lamb(30);
    }
};

Lorsque this est capturé, le lambda peut utiliser les noms de membre de sa classe contenant comme s'il était dans sa classe contenant. Donc, un implicite this-> est appliqué à ces membres.

Sachez que this est capturé par valeur, mais pas la valeur du type. Il est capturé par la valeur de this , qui est un pointeur . En tant que tel, le lambda ne possède pas this . Si le lambda out a la durée de vie de l'objet qui l'a créé, le lambda peut devenir invalide.

Cela signifie également que le lambda peut modifier this sans être déclaré mutable . C'est le pointeur qui est const , pas l'objet pointé. C'est-à-dire, à moins que la fonction membre externe ne soit elle-même une fonction const .

Sachez également que les clauses de capture par défaut, à la fois [=] et [&] , captureront également this implicitement. Et ils le capturent tous deux par la valeur du pointeur. En effet, il est erroné de spécifier this dans la liste de capture quand une valeur par défaut est donnée.

C ++ 17

Les Lambdas peuvent capturer une copie de this objet, créé au moment où le lambda est créé. Cela se fait en ajoutant *this à la liste de capture:

class Foo
{
private:
    int i;
    
public:
    Foo(int val) : i(val) {}
    
    void Test()
    {
        // capture a copy of the object given by the this pointer
        auto lamb = [*this](int val) mutable
        {
            i = val;
        };
        
        lamb(30); // does not change this->i
    }
};

Portage des fonctions lambda en C ++ 03 à l'aide de foncteurs

Les fonctions Lambda en C ++ sont des sucres syntaxiques qui fournissent une syntaxe très concise pour l'écriture des foncteurs . En tant que tel, une fonctionnalité équivalente peut être obtenue en C ++ 03 (bien que beaucoup plus verbeux) en convertissant la fonction lambda en un foncteur:

// Some dummy types:
struct T1 {int dummy;};
struct T2 {int dummy;};
struct R {int dummy;};

// Code using a lambda function (requires C++11)
R use_lambda(T1 val, T2 ref) {
  // Use auto because the type of the lambda is unknown.
  auto lambda = [val, &ref](int arg1, int arg2) -> R {
    /* lambda-body */
    return R();
  };
  return lambda(12, 27);
}

// The functor class (valid C++03)
// Similar to what the compiler generates for the lambda function.
class Functor {
  // Capture list.
  T1 val;
  T2& ref;

public:
  // Constructor
  inline Functor(T1 val, T2& ref) : val(val), ref(ref) {}

  // Functor body
  R operator()(int arg1, int arg2) const {
    /* lambda-body */
    return R();
  }
};

// Equivalent to use_lambda, but uses a functor (valid C++03).
R use_functor(T1 val, T2 ref) {
  Functor functor(val, ref);
  return functor(12, 27);
}

// Make this a self-contained example.
int main() {
  T1 t1;
  T2 t2;
  use_functor(t1,t2);
  use_lambda(t1,t2);
  return 0;
}

Si la fonction lambda est mutable faites en sorte que l'opérateur de l'appel soit non-const, c'est-à-dire:

R operator()(int arg1, int arg2) /*non-const*/ {
  /* lambda-body */
  return R();
}

Lambda récursive

Disons que nous souhaitons écrire gcd() Euclid en tant que lambda. En tant que fonction, c'est:

int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a%b);
}

Mais un lambda ne peut pas être récursif, il n'a aucun moyen d'invoquer lui-même. A lambda n'a pas de nom et d' utiliser this dans le corps d'un lambda se réfère à un capturé this ( en supposant que le lambda est créé dans le corps d'une fonction membre, sinon il est une erreur). Alors, comment pouvons-nous résoudre ce problème?

Utilisez std::function

Nous pouvons avoir une capture lambda une référence à une std::function non encore construite:

std::function<int(int, int)> gcd = [&](int a, int b){
    return b == 0 ? a : gcd(b, a%b);
};

Cela fonctionne, mais devrait être utilisé avec parcimonie. C'est lent (nous utilisons maintenant l'effacement de type au lieu d'un appel de fonction direct), c'est fragile (copier gcd ou renvoyer gcd sera cassé car le lambda fait référence à l'objet d'origine), et cela ne fonctionnera pas avec les lambdas génériques.

En utilisant deux pointeurs intelligents:

auto gcd_self = std::make_shared<std::unique_ptr< std::function<int(int, int)> >>();
*gcd_self = std::make_unique<std::function<int(int, int)>>(
  [gcd_self](int a, int b){
    return b == 0 ? a : (**gcd_self)(b, a%b);
  };
};

Cela ajoute beaucoup d'indirection (qui est une surcharge), mais il peut être copié / retourné, et toutes les copies partagent l'état. Il vous permet de retourner le lambda et est par ailleurs moins fragile que la solution ci-dessus.

Utiliser un combinateur Y

Avec l'aide d'une structure utilitaire courte, nous pouvons résoudre tous ces problèmes:

template <class F>
struct y_combinator {
    F f; // the lambda will be stored here

    // a forwarding operator():
    template <class... Args>
    decltype(auto) operator()(Args&&... args) const {
        // we pass ourselves to f, then the arguments.
        // the lambda should take the first argument as `auto&& recurse` or similar.
        return f(*this, std::forward<Args>(args)...);
    }
};
// helper function that deduces the type of the lambda:
template <class F>
y_combinator<std::decay_t<F>> make_y_combinator(F&& f) {
    return {std::forward<F>(f)};
}
// (Be aware that in C++17 we can do better than a `make_` function)

nous pouvons implémenter notre gcd comme:

auto gcd = make_y_combinator(
  [](auto&& gcd, int a, int b){
    return b == 0 ? a : gcd(b, a%b);
  }
);

Le y_combinator est un concept du calcul lambda qui vous permet d'avoir de la récursivité sans pouvoir vous nommer tant que vous n'êtes pas défini. C'est exactement le problème des lambda.

Vous créez un lambda qui prend "recurse" comme premier argument. Lorsque vous voulez vous remettre en forme, vous transmettez les arguments à récurer.

Le y_combinator retourne alors un objet fonction qui appelle cette fonction avec ses arguments, mais avec un objet "recurse" approprié (à savoir le y_combinator lui-même) comme premier argument. Il transmet également le reste des arguments que vous appelez le y_combinator au lambda.

En bref:

auto foo = make_y_combinator( [&](auto&& recurse, some arguments) {
  // write body that processes some arguments
  // when you want to recurse, call recurse(some other arguments)
});

et vous avez une récursivité dans un lambda sans restriction sérieuse ni surcharge significative.

Utilisation de lambdas pour le déballage du pack de paramètres en ligne

C ++ 14

Le déballage du paquet de paramètres nécessite traditionnellement d’écrire une fonction d’aide à chaque fois que vous le souhaitez.

Dans cet exemple de jouet:

template<std::size_t...Is>
void print_indexes( std::index_sequence<Is...> ) {
  using discard=int[];
  (void)discard{0,((void)(
    std::cout << Is << '\n' // here Is is a compile-time constant.
  ),0)...};
}
template<std::size_t I>
void print_indexes_upto() {
  return print_indexes( std::make_index_sequence<I>{} );
}

Le print_indexes_upto veut créer et décompresser un paquet de paramètres d'index. Pour ce faire, il doit appeler une fonction d'assistance. Chaque fois que vous voulez décompresser un pack de paramètres que vous avez créé, vous devez créer une fonction d'assistance personnalisée pour le faire.

Cela peut être évité avec les lambda.

Vous pouvez décompresser les paquets de paramètres dans un ensemble d'appels de lambda, comme ceci:

template<std::size_t I>
using index_t = std::integral_constant<std::size_t, I>;
template<std::size_t I>
constexpr index_t<I> index{};

template<class=void, std::size_t...Is>
auto index_over( std::index_sequence<Is...> ) {
  return [](auto&& f){
    using discard=int[];
    (void)discard{0,(void(
      f( index<Is> )
    ),0)...};
  };
}

template<std::size_t N>
auto index_over(index_t<N> = {}) {
  return index_over( std::make_index_sequence<N>{} );
}
C ++ 17

Avec les expressions de pliage, index_over() peut être simplifié pour:

template<class=void, std::size_t...Is>
auto index_over( std::index_sequence<Is...> ) {
  return [](auto&& f){
    ((void)(f(index<Is>)), ...);
  };
}

Une fois que vous avez fait cela, vous pouvez l'utiliser pour remplacer la décompression manuelle des paquets de paramètres par une seconde surcharge dans un autre code, ce qui vous permet de décompresser les paquets de paramètres "inline":

template<class Tup, class F>
void for_each_tuple_element(Tup&& tup, F&& f) {
  using T = std::remove_reference_t<Tup>;
  using std::tuple_size;
  auto from_zero_to_N = index_over< tuple_size<T>{} >();

  from_zero_to_N(
    [&](auto i){
      using std::get;
      f( get<i>( std::forward<Tup>(tup) ) );
    }
  );
}

L' auto i passée au lambda par l' index_over est un std::integral_constant<std::size_t, ???> . Cela a une conversion constexpr en std::size_t qui ne dépend pas de l'état de this , donc nous pouvons l'utiliser comme une constante de compilation, comme quand nous la passons à std::get<i> ci-dessus.

Pour revenir à l'exemple de jouet en haut, réécrivez-le comme suit:

template<std::size_t I>
void print_indexes_upto() {
  index_over(index<I>)([](auto i){
    std::cout << i << '\n'; // here i is a compile-time constant
  });
}

ce qui est beaucoup plus court, et conserve la logique dans le code qui l’utilise.

Exemple en direct avec lequel jouer.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow