web-dev-qa-db-fra.com

Lambda revenant lui-même: est-ce légal?

Considérez ce programme assez inutile:

#include <iostream>
int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto self) {
      return [&](auto b) {
        std::cout << (a + b) << std::endl;
        return self(self);
      };
  };
  it(it)(4)(6)(42)(77)(999);
}

Fondamentalement, nous essayons de faire un lambda qui retourne lui-même.

  • MSVC compile le programme et il s'exécute
  • gcc compile le programme et le sépare par défaut
  • clang rejette le programme avec un message:

    error: function 'operator()<(lambda at lam.cpp:6:13)>' with deduced return type cannot be used before it is defined

Quel compilateur a raison? Existe-t-il une violation de contrainte statique, UB ou aucune?

Mettre à jour cette légère modification est acceptée par clang:

  auto it = [&](auto& self, auto b) {
          std::cout << (a + b) << std::endl;
          return [&](auto p) { return self(self,p); };
  };
  it(it,4)(6)(42)(77)(999);

Update 2 : Je comprends comment écrire un foncteur qui se retourne lui-même ou comment utiliser le Y Combinator pour y parvenir. C'est plus une question d'avocat de langue.

Update 3 : la question est et non s'il est légal qu'un lambda se retourne en général, mais sur la légalité de cette manière spécifique de le faire.

Question associée: C++ lambda se retournant .

121
n.m.

Le programme est mal formé (clang est juste) par [dcl.spec.auto]/9 :

Si le nom d'une entité avec un type d'espace réservé non déduit apparaît dans une expression, le programme est mal formé. Cependant, une fois qu'une instruction return non rejetée a été vue dans une fonction, le type de retour déduit de cette instruction peut être utilisé dans le reste de la fonction, y compris dans d'autres instructions return.

Fondamentalement, la déduction du type de retour du lambda interne dépend de lui-même (l'entité nommée ici est l'opérateur d'appel) - vous devez donc fournir explicitement un type de retour. Dans ce cas particulier, c'est impossible, car vous avez besoin du type de lambda interne mais vous ne pouvez pas le nommer. Mais il y a d'autres cas où essayer de forcer des lambdas récursifs comme celui-ci peut fonctionner.

Même sans cela, vous avez un référence en suspens .


Laissez-moi élaborer un peu plus, après avoir discuté avec quelqu'un de beaucoup plus intelligent (c'est-à-dire T.C.) Il y a une différence importante entre le code original (légèrement réduit) et la nouvelle version proposée (également réduite):

auto f1 = [&](auto& self) {
  return [&](auto) { return self(self); } /* #1 */ ; /* #2 */
};
f1(f1)(0);

auto f2 = [&](auto& self, auto) {
  return [&](auto p) { return self(self,p); };
};
f2(f2, 0);

Et c’est que l’expression interne self(self) ne dépend pas de f1, mais self(self, p) dépend de f2. Lorsque les expressions ne sont pas dépendantes, elles peuvent être utilisées ... avec impatience ( [temp.res]/8 , par exemple, en quoi static_assert(false) est une erreur absolue, que le modèle choisi se trouve ou non. dans est instanciée ou non).

Pour f1, un compilateur (comme, disons, clang) peut essayer d'instancier cela avec impatience. Vous connaissez le type déduit du lambda extérieur une fois que vous arrivez à ce ; au point #2 ci-dessus (c'est le type du lambda intérieur), mais nous essayons de l'utiliser plus tôt que cela (pensez à comme au point #1) - nous essayons de l’utiliser pendant que nous analysons encore le lambda interne, avant de savoir de quel type il s’agit. Cela va à l'encontre de dcl.spec.auto/9.

Cependant, pour f2, nous ne pouvons pas tenter d’instancier rapidement, car cela dépend. Nous ne pouvons instancier qu'au moment de l'utilisation, alors nous savons tout.


Pour vraiment faire quelque chose comme ça, vous avez besoin d'un y-combinator . La mise en œuvre du papier:

template<class Fun>
class y_combinator_result {
    Fun fun_;
public:
    template<class T>
    explicit y_combinator_result(T &&fun): fun_(std::forward<T>(fun)) {}

    template<class ...Args>
    decltype(auto) operator()(Args &&...args) {
        return fun_(std::ref(*this), std::forward<Args>(args)...);
    }
};

template<class Fun>
decltype(auto) y_combinator(Fun &&fun) {
    return y_combinator_result<std::decay_t<Fun>>(std::forward<Fun>(fun));
}

Et ce que vous voulez c'est:

auto it = y_combinator([&](auto self, auto b){
    std::cout << (a + b) << std::endl;
    return self;
});
69
Barry

Edit : Il semble y avoir une certaine controverse sur le point de savoir si cette construction est strictement valide la spécification C++. L’opinion dominante semble être que ce n’est pas valide. Voir les autres réponses pour une discussion plus approfondie. Le reste de cette réponse s'applique if la construction est valide; le code modifié ci-dessous fonctionne avec MSVC++ et gcc, et l'OP a publié du code modifié supplémentaire qui fonctionne également avec clang.

Ceci est un comportement indéfini, car le lambda interne capture le paramètre self par référence, mais self sort de la portée après le return de la ligne 7. Ainsi, lorsque le lambda renvoyé est exécuté ultérieurement , c’est accéder à une référence à une variable qui est hors de portée.

#include <iostream>
int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto self) {
      return [&](auto b) {
        std::cout << (a + b) << std::endl;
        return self(self); // <-- using reference to 'self'
      };
  };
  it(it)(4)(6)(42)(77)(999); // <-- 'self' is now out of scope
}

L'exécution du programme avec valgrind illustre ceci:

==5485== Memcheck, a memory error detector
==5485== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5485== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5485== Command: ./test
==5485== 
9
==5485== Use of uninitialised value of size 8
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485== 
==5485== Invalid read of size 4
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485==  Address 0x4fefffdc4 is not stack'd, malloc'd or (recently) free'd
==5485== 
==5485== 
==5485== Process terminating with default action of signal 11 (SIGSEGV)
==5485==  Access not within mapped region at address 0x4FEFFFDC4
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485==  If you believe this happened as a result of a stack
==5485==  overflow in your program's main thread (unlikely but
==5485==  possible), you can try to increase the size of the
==5485==  main thread stack using the --main-stacksize= flag.
==5485==  The main thread stack size used in this run was 8388608.

Au lieu de cela, vous pouvez changer le lambda externe pour qu'il prenne soi par référence plutôt que par valeur, évitant ainsi un tas de copies inutiles et résolvant également le problème:

#include <iostream>
int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto& self) { // <-- self is now a reference
      return [&](auto b) {
        std::cout << (a + b) << std::endl;
        return self(self);
      };
  };
  it(it)(4)(6)(42)(77)(999);
}

Cela marche:

==5492== Memcheck, a memory error detector
==5492== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5492== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5492== Command: ./test
==5492== 
9
11
47
82
1004
35
TypeIA

TL; DR;

clang est correct.

Il semble que la section de la norme qui rend ce fichier mal formé est [dcl.spec.auto] p9 :

Si le nom d'une entité avec un type d'espace réservé non déduit apparaît dans une expression, le programme est mal formé. Une fois qu'une instruction return non supprimée a été vu dans une fonction, cependant, le type de retour déduit de cette instruction peut être utilisé dans le reste de la fonction, y compris dans d'autres instructions de retour. [ Exemple:

auto n = n; // error, n’s initializer refers to n
auto f();
void g() { &f; } // error, f’s return type is unknown

auto sum(int i) {
  if (i == 1)
    return i; // sum’s return type is int
  else
    return sum(i-1)+i; // OK, sum’s return type has been deduced
}

—Fin exemple]

Travail original à travers

Si nous regardons la proposition ne proposition pour ajouter Y Combinator à la bibliothèque standard , elle fournit une solution de travail:

template<class Fun>
class y_combinator_result {
    Fun fun_;
public:
    template<class T>
    explicit y_combinator_result(T &&fun): fun_(std::forward<T>(fun)) {}

    template<class ...Args>
    decltype(auto) operator()(Args &&...args) {
        return fun_(std::ref(*this), std::forward<Args>(args)...);
    }
};

template<class Fun>
decltype(auto) y_combinator(Fun &&fun) {
    return y_combinator_result<std::decay_t<Fun>>(std::forward<Fun>(fun));
}

et il est dit explicitement que votre exemple n'est pas possible:

Les lambdas de C++ 11/14 n'encouragent pas la récursion: il n'y a aucun moyen de référencer l'objet lambda à partir du corps de la fonction lambda.

et cela fait référence à un discussion dans laquelle Richard Smith fait allusion à l’erreur que clang vous donne :

Je pense que ce serait mieux en tant que fonctionnalité linguistique de première classe. Je manquais de temps pour la réunion préparatoire à Kona, mais j'avais l'intention d'écrire un document permettant de donner un nom à un lambda (son propre corps):

auto x = []fib(int a) { return a > 1 ? fib(a - 1) + fib(a - 2) : a; };

Ici, 'fib' est l'équivalent du lambda * this (avec quelques règles spéciales gênantes lui permettant de fonctionner même si le type de fermeture du lambda est incomplet).

Barry m'a indiqué la proposition de suivi lambdas récursive , ce qui explique pourquoi cela n'est pas possible et contourne la restriction dcl.spec.auto#9 et indique également les méthodes permettant d'y parvenir aujourd'hui sans cette restriction:

Les Lambda sont un outil utile pour le refactoring de code local. Cependant, nous souhaitons parfois utiliser le lambda de l'intérieur, soit pour permettre une récursion directe, soit pour permettre à la fermeture d'être enregistrée en tant que continuation. Ceci est étonnamment difficile à accomplir correctement en C++.

Exemple:

  void read(Socket sock, OutputBuffer buff) {
  sock.readsome([&] (Data data) {
  buff.append(data);
  sock.readsome(/*current lambda*/);
}).get();

}

Une tentative naturelle de référencer un lambda à partir de lui-même consiste à le stocker dans une variable et à capturer cette variable par référence:

 auto on_read = [&] (Data data) {
  buff.append(data);
  sock.readsome(on_read);
};

Cependant, ceci n'est pas possible en raison d'une circularité sémantique : le type de la variable auto n'est déduit qu'après le traitement de l'expression lambda, ce qui signifie l'expression-lambda ne peut pas référencer la variable.

Une autre approche naturelle consiste à utiliser un std :: function:

 std::function on_read = [&] (Data data) {
  buff.append(data);
  sock.readsome(on_read);
};

Cette approche compile, mais introduit généralement une pénalité d'abstraction: la fonction std :: peut encourir une allocation de mémoire et l'invocation du lambda nécessite généralement un appel indirect.

Pour une solution sans frais généraux, il n'y a souvent pas de meilleure approche que de définir explicitement un type de classe local.

21
Shafik Yaghmour

Il semble que Clang a raison. Prenons un exemple simplifié:

auto it = [](auto& self) {
    return [&self]() {
      return self(self);
    };
};
it(it);

Passons en revue comme un compilateur (un peu):

  • Le type de it est Lambda1 avec un opérateur de modèle.
  • it(it); déclenche l'instanciation de l'opérateur d'appel
  • Le type de retour de l'opérateur d'appel de modèle est auto, nous devons donc le déduire.
  • Nous retournons un lambda capturant le premier paramètre de type Lambda1.
  • Cette lambda a aussi un opérateur d'appel qui retourne le type de l'invocation self(self)
  • Remarque: self(self) est exactement ce que nous avons commencé!

En tant que tel, le type ne peut pas être déduit.

13
Rakete1111

Eh bien, votre code ne fonctionne pas. Mais cela fait:

template<class F>
struct ycombinator {
  F f;
  template<class...Args>
  auto operator()(Args&&...args){
    return f(f, std::forward<Args>(args)...);
  }
};
template<class F>
ycombinator(F) -> ycombinator<F>;

Code de test:

ycombinator bob = {[x=0](auto&& self)mutable{
  std::cout << ++x << "\n";
  ycombinator ret = {self};
  return ret;
}};

bob()()(); // prints 1 2 3

Votre code est à la fois UB et mal formé, aucun diagnostic requis. Ce qui est drôle mais les deux peuvent être fixés indépendamment.

Tout d'abord, l'UB:

auto it = [&](auto self) { // outer
  return [&](auto b) { // inner
    std::cout << (a + b) << std::endl;
    return self(self);
  };
};
it(it)(4)(5)(6);

c'est UB parce que external prend self par valeur, puis interne capture self par référence, puis le renvoie après outer. Donc, segfaulting est définitivement ok.

Le correctif:

[&](auto self) {
  return [self,&a](auto b) {
    std::cout << (a + b) << std::endl;
    return self(self);
  };
};

Le code reste est mal formé. Pour voir cela, nous pouvons développer les lambdas:

struct __outer_lambda__ {
  template<class T>
  auto operator()(T self) const {
    struct __inner_lambda__ {
      template<class B>
      auto operator()(B b) const {
        std::cout << (a + b) << std::endl;
        return self(self);
      }
      int& a;
      T self;
    };
    return __inner_lambda__{a, self};
  }
  int& a;
};
__outer_lambda__ it{a};
it(it);

cela instancie __outer_lambda__::operator()<__outer_lambda__>:

  template<>
  auto __outer_lambda__::operator()(__outer_lambda__ self) const {
    struct __inner_lambda__ {
      template<class B>
      auto operator()(B b) const {
        std::cout << (a + b) << std::endl;
        return self(self);
      }
      int& a;
      __outer_lambda__ self;
    };
    return __inner_lambda__{a, self};
  }
  int& a;
};

Nous devons ensuite déterminer le type de retour de __outer_lambda__::operator().

Nous le parcourons ligne par ligne. Tout d'abord, nous créons __inner_lambda__ type:

    struct __inner_lambda__ {
      template<class B>
      auto operator()(B b) const {
        std::cout << (a + b) << std::endl;
        return self(self);
      }
      int& a;
      __outer_lambda__ self;
    };

Regardez maintenant, son type de retour est self(self) ou __outer_lambda__(__outer_lambda__ const&). Mais nous sommes en train d'essayer de déduire le type de retour de __outer_lambda__::operator()(__outer_lambda__).

Vous n'êtes pas autorisé à faire cela.

Bien que le type de retour de __outer_lambda__::operator()(__outer_lambda__) ne dépende pas réellement du type de retour de __inner_lambda__::operator()(int), C++ ne s'en soucie pas lors de la déduction des types de retour; il vérifie simplement le code ligne par ligne.

Et self(self) est utilisé avant que nous en déduisions. Programme mal formé.

Nous pouvons corriger cela en masquant self(self) jusqu'à plus tard:

template<class A, class B>
struct second_type_helper { using result=B; };

template<class A, class B>
using second_type = typename second_type_helper<A,B>::result;

int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto self) {
      return [self,&a](auto b) {
        std::cout << (a + b) << std::endl;
        return self(second_type<decltype(b), decltype(self)&>(self) );
      };
  };
  it(it)(4)(6)(42)(77)(999);
}

et maintenant le code est correct et compile. Mais je pense que c'est un peu de bidouillage; utilisez simplement le ycombinator.

9

Il est assez facile de réécrire le code en termes de classes qu'un compilateur devrait ou devrait générer pour les expressions lambda.

Cela fait, il est clair que le problème principal n’est que la référence en suspens et qu’un compilateur qui n’accepte pas le code est un peu contesté dans le département lambda.

La réécriture montre qu'il n'y a pas de dépendances circulaires.

#include <iostream>

struct Outer
{
    int& a;

    // Actually a templated argument, but always called with `Outer`.
    template< class Arg >
    auto operator()( Arg& self ) const
        //-> Inner
    {
        return Inner( a, self );    //! Original code has dangling ref here.
    }

    struct Inner
    {
        int& a;
        Outer& self;

        // Actually a templated argument, but always called with `int`.
        template< class Arg >
        auto operator()( Arg b ) const
            //-> Inner
        {
            std::cout << (a + b) << std::endl;
            return self( self );
        }

        Inner( int& an_a, Outer& a_self ): a( an_a ), self( a_self ) {}
    };

    Outer( int& ref ): a( ref ) {}
};

int main() {

  int a = 5;

  auto&& it = Outer( a );
  it(it)(4)(6)(42)(77)(999);
}

Une version entièrement modélisée pour refléter la manière dont le lambda interne, dans le code d'origine, capture un élément de type modélisé:

#include <iostream>

struct Outer
{
    int& a;

    template< class > class Inner;

    // Actually a templated argument, but always called with `Outer`.
    template< class Arg >
    auto operator()( Arg& self ) const
        //-> Inner
    {
        return Inner<Arg>( a, self );    //! Original code has dangling ref here.
    }

    template< class Self >
    struct Inner
    {
        int& a;
        Self& self;

        // Actually a templated argument, but always called with `int`.
        template< class Arg >
        auto operator()( Arg b ) const
            //-> Inner
        {
            std::cout << (a + b) << std::endl;
            return self( self );
        }

        Inner( int& an_a, Self& a_self ): a( an_a ), self( a_self ) {}
    };

    Outer( int& ref ): a( ref ) {}
};

int main() {

  int a = 5;

  auto&& it = Outer( a );
  it(it)(4)(6)(42)(77)(999);
}

J'imagine que c'est ce modèle dans la machine interne, que les règles formelles sont conçues pour interdire. S'ils interdisent la construction originale.

7