Lambda che ritorna da sola: è legale?


124

Considera questo programma abbastanza 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);
}

Fondamentalmente stiamo cercando di creare un lambda che ritorni da solo.

  • MSVC compila il programma e viene eseguito
  • gcc compila il programma e esegue il segfault
  • clang rifiuta il programma con un messaggio:

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

Quale compilatore ha ragione? C'è una violazione del vincolo statico, UB o nessuno dei due?

Aggiorna questa leggera modifica è accettata da 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);

Aggiornamento 2 : capisco come scrivere un funtore che restituisce se stesso, o come usare il combinatore Y, per ottenere ciò. Questa è più una domanda da avvocato linguistico.

Aggiornamento 3 : la domanda non è se sia legale che un lambda ritorni da solo in generale, ma sulla legalità di questo modo specifico di farlo.

Domanda correlata: lambda C ++ che si restituisce .


2
clang sembra più decente in questo momento, mi chiedo se un tale costrutto possa anche tipecheck, più probabilmente finisce in un albero infinito.
bipll il

2
La tua domanda se è legale, il che dice che questa è una domanda di un avvocato linguistico, ma molte delle risposte non adottano davvero questo approccio ... è importante che i tag siano corretti
Shafik Yaghmour

2
@ShafikYaghmour Grazie, aggiunto un tag
n. 'pronomi' m.

1
@ArneVogel si utilizza quello aggiornato auto& selfche elimina il problema di riferimento penzolante.
n. 'pronomi' m.

1
@TheGreatDuck i lambda C ++ non sono espressioni lambda realmente teoriche. Il C ++ ha tipi ricorsivi incorporati che il semplice lambda calcolo tipizzato originale non può esprimere, quindi può avere cose isomorfiche ad a: a-> a e altri costrutti impossibili.
n. 'pronomi' m.

Risposte:


68

Il programma è mal formato (clang è giusto) per [dcl.spec.auto] / 9 :

Se il nome di un'entità con un tipo di segnaposto non ridotto appare in un'espressione, il programma è mal formato. Tuttavia, una volta che un'istruzione return non scartata è stata vista in una funzione, il tipo di ritorno dedotto da tale istruzione può essere utilizzato nel resto della funzione, incluse altre istruzioni return.

Fondamentalmente, la detrazione del tipo di ritorno del lambda interno dipende da se stessa (l'entità che viene nominata qui è l'operatore di chiamata), quindi devi fornire esplicitamente un tipo di ritorno. In questo caso particolare, è impossibile, perché è necessario il tipo di lambda interno ma non è possibile nominarlo. Ma ci sono altri casi in cui provare a forzare lambda ricorsivi come questo, può funzionare.

Anche senza quello, hai un riferimento penzolante .


Lasciatemi approfondire ancora, dopo aver discusso con qualcuno molto più intelligente (cioè TC) C'è un'importante differenza tra il codice originale (leggermente ridotto) e la nuova versione proposta (anch'essa ridotta):

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);

E cioè che l'espressione interiore self(self)non dipende da f1, ma self(self, p)dipende daf2 . Quando le espressioni non sono dipendenti, possono essere utilizzate ... con entusiasmo ( [temp.res] / 8 , ad esempio come static_assert(false)è un errore difficile indipendentemente dal fatto che il modello in cui si trova sia istanziato o meno).

Perché f1un compilatore (come, diciamo, clang) può provare a istanziarlo con entusiasmo. Conosci il tipo dedotto del lambda esterno una volta arrivato a quello; al punto #2sopra (è il tipo del lambda interno), ma stiamo cercando di usarlo prima (pensalo come al punto#1 ) - ci stiamo provando per usarlo mentre stiamo ancora analizzando il lambda interno, prima di sapere qual è effettivamente il tipo. Ciò va contro dcl.spec.auto/9.

Tuttavia, per f2 non possiamo provare a creare un'istanza con entusiasmo, perché dipende. Possiamo solo istanziare al punto di utilizzo, a quel punto sappiamo tutto.


Per fare davvero qualcosa di simile, hai bisogno di un combinatore y . L'implementazione dal documento:

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));
}

E quello che vuoi è:

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

Come si specifica esplicitamente il tipo di ritorno? Non riesco a capirlo.
Rakete1111

@ Rakete1111 Quale? Nell'originale non puoi.
Barry

Oh va bene. Non sono un nativo, ma "quindi devi fornire esplicitamente un tipo di ritorno" sembra implicare che esiste un modo, ecco perché lo stavo chiedendo :)
Rakete1111

4
@PedroA stackoverflow.com/users/2756719/tc è un collaboratore C ++. Inoltre, non è un AI, o abbastanza intraprendente da convincere un umano che conosce anche il C ++ a partecipare al recente mini-meeting LWG a Chicago.
Casey

3
@Casey O forse l'umano sta solo ripetendo a pappagallo ciò che l'IA gli ha detto ... non si sa mai;)
TC

34

Modifica : sembra esserci qualche controversia sul fatto che questa costruzione sia strettamente valida per la specifica C ++. L'opinione prevalente sembra essere che non sia valida. Vedi le altre risposte per una discussione più approfondita. Il resto di questa risposta si applica se la costruzione è valida; il codice modificato di seguito funziona con MSVC ++ e gcc, e l'OP ha pubblicato un ulteriore codice modificato che funziona anche con clang.

Questo è un comportamento indefinito, perché il lambda interno cattura il parametro selfper riferimento, ma selfesce dall'ambito dopo la returnriga 7. Pertanto, quando il lambda restituito viene eseguito in seguito, accede a un riferimento a una variabile che è uscito dall'ambito.

#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'esecuzione del programma con valgrindillustra questo:

==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.

Invece puoi cambiare il lambda esterno per prendere sé per riferimento invece che per valore, evitando così un mucchio di copie non necessarie e risolvendo anche il problema:

#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);
}

Funziona:

==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

Non ho familiarità con i lambda generici, ma non potresti fare selfun riferimento?
François Andrieux

@ FrançoisAndrieux Sì, se fai selfun riferimento, questo problema scompare , ma Clang lo rifiuta
Justin

@ FrançoisAndrieux Infatti e l'ho aggiunto alla risposta, grazie!
TypeIA

Il problema con questo approccio è che non elimina i possibili bug del compilatore. Quindi forse dovrebbe funzionare ma l'implementazione non funziona.
Shafik Yaghmour

Grazie, l'ho guardato per ore e non ho visto che selfè catturato per riferimento!
n. 'pronomi' m.

21

TL; DR;

clang è corretto.

Sembra che la sezione dello standard che rende questo malformato sia [dcl.spec.auto] p9 :

Se il nome di un'entità con un tipo di segnaposto non ridotto appare in un'espressione, il programma è mal formato. Tuttavia, una volta che un'istruzione return non scartata è stata vista in una funzione, il tipo di ritorno dedotto da tale istruzione può essere utilizzato nel resto della funzione, incluse altre istruzioni return. [ Esempio:

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
}

—End esempio]

Lavoro originale attraverso

Se guardiamo alla proposta Una proposta per aggiungere Y Combinator alla libreria standard , fornisce una soluzione funzionante:

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));
}

e dice esplicitamente che il tuo esempio non è possibile:

I lambda C ++ 11/14 non incoraggiano la ricorsione: non c'è modo di fare riferimento all'oggetto lambda dal corpo della funzione lambda.

e fa riferimento a una discussione in cui Richard Smith allude all'errore che il clangore ti sta dando :

Penso che questo sarebbe meglio come funzionalità linguistica di prima classe. Ho esaurito il tempo per l'incontro pre-Kona, ma avevo intenzione di scrivere un documento per consentire di dare un nome a un lambda (mirato al proprio corpo):

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

Qui, 'fib' è l'equivalente di lambda * this (con alcune regole speciali fastidiose per consentire che funzioni nonostante il tipo di chiusura lambda sia incompleto).

Barry mi ha indicato la proposta di follow-up Lambda ricorsive che spiega perché ciò non è possibile e aggira la dcl.spec.auto#9restrizione e mostra anche i metodi per raggiungere questo obiettivo oggi senza di essa:

Lambda sono uno strumento utile per il refactoring del codice locale. Tuttavia, a volte si desidera utilizzare il lambda dall'interno di se stesso, per consentire la ricorsione diretta o per consentire la registrazione della chiusura come continuazione. Questo è sorprendentemente difficile da realizzare bene nell'attuale C ++.

Esempio:

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

}

Un tentativo naturale di fare riferimento a un lambda da se stesso è memorizzarlo in una variabile e catturarla per riferimento:

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

Tuttavia, questo non è possibile a causa di una circolarità semantica : il tipo della variabile auto non viene dedotto fino a dopo l'elaborazione dell'espressione lambda, il che significa che l'espressione lambda non può fare riferimento alla variabile.

Un altro approccio naturale è usare una funzione std :::

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

Questo approccio viene compilato, ma in genere introduce una penalità di astrazione: la funzione std :: può incorrere in un'allocazione di memoria e l'invocazione di lambda richiederà in genere una chiamata indiretta.

Per una soluzione zero overhead, spesso non esiste un approccio migliore della definizione esplicita di un tipo di classe locale.


@ Cheersandhth.-Alf Ho finito per trovare la citazione standard dopo aver letto il giornale, quindi non è rilevante poiché la citazione standard chiarisce perché nessuno dei due approcci funziona
Shafik Yaghmour

"" Se il nome di un'entità con un tipo di segnaposto non ridotto appare in un'espressione, il programma è mal formato "Non vedo un'occorrenza di questo nel programma anche selfse non sembra un'entità del genere.
n. "pronomi" m.

@nm oltre alla possibile formulazione lendini, gli esempi sembrano avere senso con la formulazione e credo che gli esempi dimostrino chiaramente il problema. Non credo di poter aggiungere altro al momento per aiutare.
Shafik Yaghmour

13

Sembra che il clang sia giusto. Considera un esempio semplificato:

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

Analizziamolo come un compilatore (un po '):

  • Il tipo di itè Lambda1con un operatore di chiamata modello.
  • it(it); attiva la creazione di istanze dell'operatore di chiamata
  • Il tipo di ritorno dell'operatore di chiamata del modello è auto, quindi dobbiamo dedurlo.
  • Restituiamo un lambda che cattura il primo parametro di tipo Lambda1.
  • Quel lambda ha anche un operatore di chiamata che restituisce il tipo di invocazione self(self)
  • Avviso: self(self)è esattamente ciò con cui abbiamo iniziato!

In quanto tale, il tipo non può essere dedotto.


Il tipo restituito di Lambda1::operator()è semplicemente Lambda2. Quindi all'interno di quell'espressione lambda interna è noto che anche il tipo restituito di self(self), una chiamata di . Forse le regole formali ostacolano questa banale deduzione, ma la logica qui presentata non lo è. La logica qui equivale solo a un'affermazione. Se le regole formali sono d'intralcio, allora è un difetto nelle regole formali. Lambda1::operator()Lambda2
Saluti e salute. - Alf

@ Cheersandhth.-Alf Sono d'accordo che il tipo di ritorno è Lambda2, ma sai che non puoi avere un operatore di chiamata non ridotto solo perché, perché questo è ciò che stai proponendo: Ritarda la detrazione del tipo di ritorno dell'operatore di chiamata di Lambda2. Ma non puoi cambiare le regole per questo, poiché è piuttosto fondamentale.
Rakete1111

9

Bene, il tuo codice non funziona. Ma questo fa:

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>;

Codice di prova:

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

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

Il tuo codice è sia UB che mal formato, non è richiesta alcuna diagnostica. Che è divertente; ma entrambi possono essere riparati indipendentemente.

Innanzitutto, 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);

questo è UB perché outer prende selfper valore, quindi inner acquisisce selfper riferimento, quindi procede a restituirlo al outertermine dell'esecuzione. Quindi il segfault è decisamente ok.

La correzione:

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

Il codice rimane è mal formato. Per vedere questo possiamo espandere i lambda:

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);

questo istanzia __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;
};

Quindi dobbiamo determinare il tipo di ritorno di __outer_lambda__::operator() .

Lo esaminiamo riga per riga. Per prima cosa creiamo__inner_lambda__ tipo:

    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;
    };

Ora, guarda lì: il suo tipo di ritorno è self(self) , o __outer_lambda__(__outer_lambda__ const&). Ma siamo nel mezzo del tentativo di dedurre il tipo di restituzione di__outer_lambda__::operator()(__outer_lambda__) .

Non ti è permesso farlo.

Mentre in effetti il ​​tipo di ritorno di __outer_lambda__::operator()(__outer_lambda__) non dipende dal tipo restituito di__inner_lambda__::operator()(int) , C ++ non si preoccupa quando si deducono i tipi restituiti; controlla semplicemente il codice riga per riga.

E self(self) è usato prima che lo deducessimo. Programma mal formato.

Possiamo aggiustarlo nascondendoci self(self)fino a più tardi:

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);
}

e ora il codice è corretto e viene compilato. Ma penso che questo sia un po 'di hack; usa semplicemente ycombinator.


Forse (IDK) questa descrizione è corretta per le regole formali sui lambda. Ma in termini di riscrittura del modello, il tipo restituito del modello lambda interno operator()non può in generale essere dedotto fino a quando non viene istanziato (essendo chiamato con qualche argomento di qualche tipo). E quindi una riscrittura manuale simile a una macchina in codice basato su modello funziona bene.
Saluti e salute. - Alf

@cheers il tuo codice è diverso; inner è una classe template nel tuo codice, ma non è nel codice my o OP. E questo è importante, poiché i metodi della classe modello vengono istanziati in ritardo fino a quando non vengono chiamati.
Yakk - Adam Nevraumont

Una classe definita all'interno di una funzione basata su modelli è equivalente a una classe basata su modelli esterna a tale funzione. Definirlo all'esterno della funzione è necessario per il codice demo quando ha una funzione membro basata su modelli, poiché le regole C ++ non consentono un modello di membro in una classe definita dall'utente locale. Quella restrizione formale non vale per qualunque cosa il compilatore stesso genera.
Saluti e salute. - Alf

7

È abbastanza facile riscrivere il codice in termini di classi che un compilatore dovrebbe, o meglio dovrebbe, generare per le espressioni lambda.

Una volta fatto, è chiaro che il problema principale è solo il riferimento penzolante e che un compilatore che non accetta il codice è in qualche modo messo alla prova nel reparto lambda.

La riscrittura mostra che non ci sono dipendenze circolari.

#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);
}

Una versione completamente basata su modelli per riflettere il modo in cui il lambda interno nel codice originale acquisisce un elemento di tipo basato su modelli:

#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);
}

Immagino che sia questo modello nella macchina interna, che le regole formali sono progettate per vietare. Se vietano il costrutto originale.


Vedi, il problema è che template< class > class Inner;il modello operator()è ... istanziato? Beh, parola sbagliata. Scritto? ... durante Outer::operator()<Outer>prima che venga dedotto il tipo di ritorno dell'operatore esterno. E Inner<Outer>::operator()ha una chiamata a Outer::operator()<Outer>se stessa. E questo non è permesso. Ora, la maggior parte dei compilatori non nota il self(self)perché attende di dedurre il tipo restituito di Outer::Inner<Outer>::operator()<int>quando intviene passato. Sensibile. Ma manca la malformazione del codice.
Yakk - Adam Nevraumont

Bene, penso che debbano aspettare per dedurre il tipo di ritorno del modello di funzione fino a quando non Innner<T>::operator()<U>viene istanziato quel modello di funzione . Dopo tutto il tipo di ritorno potrebbe dipendere da Uhere. Non è così, ma in generale.
Saluti e salute. - Alf

sicuro; ma qualsiasi espressione il cui tipo è determinato da una detrazione di tipo restituito incompleta rimane illegale. Solo alcuni compilatori sono pigri e non controllano più tardi, a quel punto tutto funziona.
Yakk - Adam Nevraumont
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.