Perché progettare una lingua con tipi anonimi unici?


91

Questo è qualcosa che mi ha sempre disturbato come caratteristica delle espressioni lambda C ++: il tipo di espressione lambda C ++ è unico e anonimo, semplicemente non riesco a scriverlo. Anche se creo due lambda sintatticamente identici, i tipi risultanti sono definiti per essere distinti. La conseguenza è che a) i lambda possono essere passati solo alle funzioni modello che consentono il tempo di compilazione, il tipo indicibile da passare insieme all'oggetto, eb) che i lambda sono utili solo una volta cancellati dal tipo std::function<>.

Ok, ma questo è solo il modo in cui lo fa C ++, ero pronto a scriverlo come una caratteristica fastidiosa di quel linguaggio. Tuttavia, ho appena scoperto che Rust apparentemente fa lo stesso: ogni funzione Rust o lambda ha un tipo unico e anonimo. E ora mi chiedo: perché?

Quindi, la mia domanda è questa:
qual è il vantaggio, dal punto di vista di un designer di linguaggi, di introdurre il concetto di un tipo unico e anonimo in una lingua?


6
come sempre la domanda migliore è perché no.
Stargateur

31
"che i lambda sono utili solo una volta cancellati tramite std :: function <>" - no, sono direttamente utili senza std::function. Un lambda che è stato passato a una funzione template può essere chiamato direttamente senza coinvolgere std::function. Il compilatore può quindi inline il lambda nella funzione template che migliorerà l'efficienza di runtime.
Erlkoenig

1
La mia ipotesi, rende più facile l'implementazione di lambda e rende la lingua più facile da capire. Se hai consentito di piegare la stessa identica espressione lambda nello stesso tipo, avresti bisogno di regole speciali da gestire { int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }poiché la variabile a cui si riferisce è diversa, anche se testualmente sono le stesse. Se dici solo che sono tutti unici, non devi preoccuparti di cercare di capirlo.
NathanOliver

5
ma puoi anche dare un nome a un tipo lambda e fare lo stesso con quello. lambdas_type = decltype( my_lambda);
grande_prime_is_463035818

3
Ma quale dovrebbe essere un tipo di lambda generico [](auto) {}? Dovrebbe avere un tipo, per cominciare?
Evg

Risposte:


77

Molti standard (specialmente C ++) adottano l'approccio di ridurre al minimo quanto richiedono dai compilatori. Francamente, chiedono già abbastanza! Se non devono specificare qualcosa per farlo funzionare, hanno la tendenza a lasciare l'implementazione definita.

Se i lambda non fossero anonimi, dovremmo definirli. Questo dovrebbe dire molto su come vengono catturate le variabili. Considera il caso di un lambda [=](){...}. Il tipo dovrebbe specificare quali tipi sono stati effettivamente catturati dal lambda, il che potrebbe essere non banale da determinare. Inoltre, cosa succede se il compilatore ottimizza con successo una variabile? Ritenere:

static const int i = 5;
auto f = [i]() { return i; }

Un compilatore ottimizzato potrebbe facilmente riconoscere che l'unico valore possibile di iquello che potrebbe essere catturato è 5 e sostituirlo con auto f = []() { return 5; }. Tuttavia, se il tipo non è anonimo, questo potrebbe cambiare il tipo o costringere il compilatore a ottimizzare di meno, archiviando ianche se in realtà non ne aveva bisogno. Questo è un intero bagaglio di complessità e sfumature che semplicemente non è necessario per ciò che i lambda avrebbero dovuto fare.

E, nel caso in cui tu abbia effettivamente bisogno di un tipo non anonimo, puoi sempre costruire tu stesso la classe di chiusura e lavorare con un funtore piuttosto che con una funzione lambda. Pertanto, possono fare in modo che i lambda gestiscano il caso del 99% e ti lasciano codificare la tua soluzione nell'1%.


Deduplicator ha sottolineato nei commenti che non ho affrontato l'unicità quanto l'anonimato. Sono meno certo dei vantaggi dell'unicità, ma vale la pena notare che il comportamento di quanto segue è chiaro se i tipi sono unici (l'azione sarà istanziata due volte).

int counter()
{
    static int count = 0;
    return count++;
}

template <typename FuncT>
void action(const FuncT& func)
{
    static int ct = counter();
    func(ct);
}

...
for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

Se i tipi non fossero univoci, dovremmo specificare quale comportamento dovrebbe verificarsi in questo caso. Potrebbe essere complicato. Alcune delle questioni che sono state sollevate sul tema dell'anonimato sollevano anche la loro brutta testa in questo caso per l'unicità.


Si noti che non si tratta veramente di salvare il lavoro per un implementatore del compilatore, ma di salvare il lavoro per il responsabile degli standard. Il compilatore deve ancora rispondere a tutte le domande precedenti per la sua specifica implementazione, ma non sono specificate nello standard.
ComicSansMS

2
@ComicSansMS Mettere insieme queste cose quando si implementa un compilatore è molto più semplice quando non è necessario adattare la propria implementazione allo standard di qualcun altro. Parlando per esperienza, è spesso molto più facile per un manutentore di standard specificare in modo eccessivo la funzionalità piuttosto che cercare di trovare la quantità minima da specificare mentre si estrae la funzionalità desiderata dalla propria lingua. Come eccellente caso di studio, guarda quanto lavoro hanno speso per evitare di specificare eccessivamente memory_order_consume rendendolo comunque utile (su alcune architetture)
Cort Ammon

1
Come tutti gli altri, difendi in modo convincente l' anonimo . Ma è davvero una buona idea costringerlo a essere anche unico ?
Deduplicatore

Non è la complessità del compilatore che conta qui, ma la complessità del codice generato. Il punto non è rendere il compilatore più semplice, ma dargli abbastanza spazio per ottimizzare tutti i casi e produrre codice naturale per la piattaforma di destinazione.
Jan Hudec

Non puoi catturare una variabile statica.
Ruslan

69

I lambda non sono solo funzioni, sono una funzione e uno stato . Pertanto sia C ++ che Rust li implementano come un oggetto con un operatore di chiamata ( operator()in C ++, i 3 Fn*tratti in Rust).

Fondamentalmente, [a] { return a + 1; }in C ++ desugars a qualcosa di simile

struct __SomeName {
    int a;

    int operator()() {
        return a + 1;
    }
};

quindi utilizzando un'istanza in __SomeNamecui viene utilizzato lambda.

Mentre in Rust, || a + 1in Rust desugar a qualcosa di simile

{
    struct __SomeName {
        a: i32,
    }

    impl FnOnce<()> for __SomeName {
        type Output = i32;
        
        extern "rust-call" fn call_once(self, args: ()) -> Self::Output {
            self.a + 1
        }
    }

    // And FnMut and Fn when necessary

    __SomeName { a }
}

Ciò significa che la maggior parte dei lambda deve avere tipi diversi .

Ora, ci sono alcuni modi per farlo:

  • Con i tipi anonimi, che è ciò che entrambi i linguaggi implementano. Un'altra conseguenza di ciò è che tutti i lambda devono avere un tipo diverso. Ma per i progettisti del linguaggio, questo ha un chiaro vantaggio: Lambda può essere semplicemente descritto utilizzando altre parti più semplici già esistenti del linguaggio. Sono solo zucchero di sintassi attorno a parti già esistenti del linguaggio.
  • Con una sintassi speciale per la denominazione dei tipi lambda: questo tuttavia non è necessario poiché lambda può già essere utilizzato con i modelli in C ++ o con i generici ei Fn*tratti in Rust. Nessuno dei due linguaggi ti obbliga mai a cancellare i caratteri lambda per usarli (con std::functionin C ++ o Box<Fn*>in Rust).

Si noti inoltre che entrambi i linguaggi concordano sul fatto che banali lambda che non catturano il contesto possono essere convertiti in puntatori a funzione.


Descrivere caratteristiche complesse di un linguaggio utilizzando funzionalità più semplici è piuttosto comune. Ad esempio, sia C ++ che Rust hanno cicli range-for, ed entrambi li descrivono come zucchero di sintassi per altre funzionalità.

C ++ definisce

for (auto&& [first,second] : mymap) {
    // use first and second
}

come equivalente a

{

    init-statement
    auto && __range = range_expression ;
    auto __begin = begin_expr ;
    auto __end = end_expr ;
    for ( ; __begin != __end; ++__begin) {

        range_declaration = *__begin;
        loop_statement

    }

} 

e Rust definisce

for <pat> in <head> { <body> }

come equivalente a

let result = match ::std::iter::IntoIterator::into_iter(<head>) {
    mut iter => {
        loop {
            let <pat> = match ::std::iter::Iterator::next(&mut iter) {
                ::std::option::Option::Some(val) => val,
                ::std::option::Option::None => break
            };
            SemiExpr(<body>);
        }
    }
};

che mentre sembrano più complicati per un essere umano, sono entrambi più semplici per un progettista di linguaggi o un compilatore.


15
@ cmaster-reinstatemonica Considera l'idea di passare un lambda come argomento di confronto per una funzione di ordinamento. Vorresti davvero imporre un sovraccarico di chiamate di funzioni virtuali qui?
Daniel Langr

5
@ cmaster-reinstatemonica perché nulla è virtuale per impostazione predefinita in C ++
Caleth

4
@cmaster - Intendi costringere tutti gli utenti di lambda a pagare per dipatch dinamico, anche quando non ne avranno bisogno?
StoryTeller - Unslander Monica

4
@ cmaster-reinstatemonica Il meglio che otterrai è opt-in per virtuale. Indovina un po ', std::functionfa quello
Caleth

9
@ cmaster-reinstatemonica qualsiasi meccanismo in cui è possibile puntare nuovamente la funzione da chiamare avrà situazioni con sovraccarico di runtime. Questo non è il modo in C ++. std::function
Accetti

13

(In aggiunta alla risposta di Caleth, ma troppo lungo per rientrare in un commento.)

L'espressione lambda è solo zucchero sintattico per una struttura anonima (un tipo Voldemort, perché non puoi pronunciare il suo nome).

Puoi vedere la somiglianza tra una struttura anonima e l'anonimato di un lambda in questo snippet di codice:

#include <iostream>
#include <typeinfo>

using std::cout;

int main() {
    struct { int x; } foo{5};
    struct { int x; } bar{6};
    cout << foo.x << " " << bar.x << "\n";
    cout << typeid(foo).name() << "\n";
    cout << typeid(bar).name() << "\n";
    auto baz = [x = 7]() mutable -> int& { return x; };
    auto quux = [x = 8]() mutable -> int& { return x; };
    cout << baz() << " " << quux() << "\n";
    cout << typeid(baz).name() << "\n";
    cout << typeid(quux).name() << "\n";
}

Se questo è ancora insoddisfacente per un lambda, dovrebbe essere altrettanto insoddisfacente per una struttura anonima.

Alcuni linguaggi consentono una sorta di dattilografia che è un po 'più flessibile, e anche se C ++ ha modelli che non aiutano davvero a creare un oggetto da un modello che ha un campo membro che può sostituire direttamente un lambda piuttosto che usare un std::functioninvolucro.


3
Grazie, questo in effetti getta un po 'di luce sul ragionamento alla base del modo in cui i lambda sono definiti in C ++ (devo ricordare il termine "tipo Voldemort" :-)). Tuttavia, la domanda rimane: qual è il vantaggio di questo agli occhi di un progettista di lingue?
cmaster - ripristina monica il

1
Potresti anche aggiungere int& operator()(){ return x; }a quelle strutture
Caleth

2
@ cmaster-reinstatemonica • Speculativamente ... il resto di C ++ si comporta in questo modo. Per fare in modo che i lambda usino una sorta di "forma di superficie", la digitazione a papera sarebbe qualcosa di molto diverso dal resto della lingua. L'aggiunta di quel tipo di facilità nella lingua per le lambda sarebbe probabilmente considerata generalizzata per l'intera lingua e sarebbe un cambiamento potenzialmente enorme. Omettere una tale funzionalità solo per i lambda si adatta alla tipizzazione fortemente ish del resto del C ++.
Eljay

Tecnicamente un tipo Voldemort sarebbe auto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }, perché fuori dal foonulla può usare l'identificatoreDarkLord
Caleth

@ cmaster-reinstatemonica efficienza, l'alternativa sarebbe quella di inscatolare e inviare dinamicamente ogni lambda (allocalo sull'heap e cancella il suo tipo preciso). Ora, come noti, il compilatore potrebbe deduplicare i tipi anonimi di lambda, ma non saresti comunque in grado di scriverli e richiederebbe un lavoro significativo per un guadagno minimo, quindi le probabilità non sono davvero favorevoli.
Masklinn

10

Perché progettare una lingua con tipi anonimi unici ?

Perché ci sono casi in cui i nomi sono irrilevanti e non utili o addirittura controproducenti. In questo caso la capacità di astrarre la loro esistenza è utile perché riduce l'inquinamento dei nomi e risolve uno dei due difficili problemi dell'informatica (come nominare le cose). Per lo stesso motivo, gli oggetti temporanei sono utili.

lambda

L'unicità non è una cosa speciale lambda, o anche una cosa speciale per i tipi anonimi. Si applica anche ai tipi con nome nella lingua. Considera quanto segue:

struct A {
    void operator()(){};
};

struct B {
    void operator()(){};
};

void foo(A);

Nota che non posso passare Bin foo, anche se le classi sono identiche. Questa stessa proprietà si applica ai tipi senza nome.

lambda può essere passato solo a funzioni modello che consentono di passare il tempo di compilazione, tipo indicibile insieme all'oggetto ... cancellato tramite std :: function <>.

Esiste una terza opzione per un sottoinsieme di espressioni lambda: le espressioni lambda non acquisite possono essere convertite in puntatori a funzione.


Si noti che se le limitazioni di un tipo anonimo sono un problema per un caso d'uso, la soluzione è semplice: al suo posto è possibile utilizzare un tipo denominato. Lambdas non fa nulla che non possa essere fatto con una classe denominata.


10

La risposta accettata da Cort Ammon è buona, ma penso che ci sia un altro punto importante da sottolineare sull'implementazione.

Supponiamo che io abbia due diverse unità di traduzione, "one.cpp" e "two.cpp".

// one.cpp
struct A { int operator()(int x) const { return x+1; } };
auto b = [](int x) { return x+1; };
using A1 = A;
using B1 = decltype(b);

extern void foo(A1);
extern void foo(B1);

I due overload di fooutilizzano lo stesso identificatore ( foo) ma hanno nomi alterati diversi. (Nell'ABI Itanium utilizzato sui sistemi POSIX, i nomi alterati sono _Z3foo1Ae, in questo caso particolare,. _Z3fooN1bMUliE_E)

// two.cpp
struct A { int operator()(int x) const { return x + 1; } };
auto b = [](int x) { return x + 1; };
using A2 = A;
using B2 = decltype(b);

void foo(A2) {}
void foo(B2) {}

Il compilatore C ++ deve assicurarsi che il nome void foo(A1)alterato di "two.cpp" sia lo stesso del nome extern void foo(A2)alterato di "one.cpp", in modo che possiamo collegare insieme i due file oggetto. Questo è il significato fisico di due tipi che sono "lo stesso tipo": si tratta essenzialmente di compatibilità ABI tra file oggetto compilati separatamente.

Il compilatore C ++ non è necessario per garantire che B1e B2siano "dello stesso tipo". (In effetti, è necessario assicurarsi che siano di tipi diversi; ma non è così importante in questo momento.)


Quale meccanismo fisico utilizza il compilatore per garantire che A1e A2siano "dello stesso tipo"?

Si insinua semplicemente attraverso i typedef e quindi esamina il nome completo del tipo. È un tipo di classe denominato A. (Bene, ::Adato che è nello spazio dei nomi globale.) Quindi è dello stesso tipo in entrambi i casi. È facile da capire. Ancora più importante, è facile da implementare . Per vedere se due tipi di classe sono dello stesso tipo, prendi i loro nomi e fai un strcmp. Per manipolare un tipo di classe nel nome alterato di una funzione, scrivi il numero di caratteri nel suo nome, seguito da quei caratteri.

Quindi, i tipi con nome sono facili da manipolare.

Quale meccanismo fisico potrebbe utilizzare il compilatore per garantire che B1e B2siano "dello stesso tipo", in un mondo ipotetico in cui C ++ richiede che siano dello stesso tipo?

Bene, non potrebbe usare il nome del tipo, perché il tipo non ha un nome.

Forse potrebbe in qualche modo codificare il testo del corpo del lambda. Ma sarebbe un po 'imbarazzante, perché in realtà b"one.cpp" è leggermente diverso da b"two.cpp": "one.cpp" ha x+1e "two.cpp" ha x + 1. Così avremmo dovuto trovare una regola che dice che questa differenza sia gli spazi non lo fa materia, o che fa (che li rende diversi tipi dopo tutto), o che forse lo fa (forse la validità del programma è definito dall'implementazione , o forse è "mal formato nessuna diagnosi richiesta"). Comunque,A

Il modo più semplice per uscire dalla difficoltà è semplicemente dire che ogni espressione lambda produce valori di un tipo univoco. Quindi due tipi lambda definiti in unità di traduzione diverse non sono sicuramente lo stesso tipo . All'interno di una singola unità di traduzione, possiamo "nominare" i tipi lambda semplicemente contando dall'inizio del codice sorgente:

auto a = [](){};  // a has type $_0
auto b = [](){};  // b has type $_1
auto f(int x) {
    return [x](int y) { return x+y; };  // f(1) and f(2) both have type $_2
} 
auto g(float x) {
    return [x](int y) { return x+y; };  // g(1) and g(2) both have type $_3
} 

Ovviamente questi nomi hanno un significato solo all'interno di questa unità di traduzione. Questa TU $_0è sempre di un tipo diverso da altre TU $_0, anche se questa TU struct Aè sempre lo stesso tipo di altre TU struct A.

A proposito, nota che la nostra idea di "codificare il testo del lambda" aveva un altro sottile problema: i lambda $_2e $_3consistono esattamente dello stesso testo , ma chiaramente non dovrebbero essere considerati dello stesso tipo!


A proposito, C ++ richiede che il compilatore sappia come manipolare il testo di un'espressione C ++ arbitraria , come in

template<class T> void foo(decltype(T())) {}
template void foo<int>(int);  // _Z3fooIiEvDTcvT__EE, not _Z3fooIiEvT_

Ma C ++ non (ancora) richiede al compilatore di sapere come manipolare un arbitrario C ++ dichiarazione . decltype([](){ ...arbitrary statements... })è ancora mal formato anche in C ++ 20.


Notare anche che è facile dare un alias locale a un tipo senza nome usando typedef/ using. Ho la sensazione che la tua domanda possa essere nata dal tentativo di fare qualcosa che potrebbe essere risolto in questo modo.

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

// Give the type an alias, so I can refer to it within this translation unit
using AdderLambda = decltype(f(0));

int of_one(AdderLambda g) { return g(1); }

int main() {
    auto f1 = f(1);
    assert(of_one(f1) == 2);
    auto f42 = f(42);
    assert(of_one(f42) == 43);
}

MODIFICATO PER AGGIUNGERE: Dalla lettura di alcuni dei tuoi commenti su altre risposte, sembra che ti stia chiedendo perché

int add1(int x) { return x + 1; }
int add2(int x) { return x + 2; }
static_assert(std::is_same_v<decltype(add1), decltype(add2)>);
auto add3 = [](int x) { return x + 3; };
auto add4 = [](int x) { return x + 4; };
static_assert(not std::is_same_v<decltype(add3), decltype(add4)>);

Questo perché i lambda senza cattura sono costruibili per impostazione predefinita. (In C ++ solo a partire da C ++ 20, ma è sempre stato concettualmente vero.)

template<class T>
int default_construct_and_call(int x) {
    T t;
    return t(x);
}

assert(default_construct_and_call<decltype(add3)>(42) == 45);
assert(default_construct_and_call<decltype(add4)>(42) == 46);

Se ci provassi default_construct_and_call<decltype(&add1)>, tsarebbe un puntatore a funzione inizializzato di default e probabilmente saresti segfault. Non è utile.


" In effetti, è necessario assicurarsi che siano tipi diversi; ma non è così importante in questo momento. " Mi chiedo se ci sia una buona ragione per forzare l'unicità se definita in modo equivalente.
Deduplicator

Personalmente penso che un comportamento completamente definito sia (quasi?) Sempre migliore di un comportamento non specificato. "Questi due puntatori a funzione sono uguali? Beh, solo se queste due istanze di template sono la stessa funzione, il che è vero solo se questi due tipi lambda sono dello stesso tipo, il che è vero solo se il compilatore ha deciso di unirli." Icky! (Ma si noti che abbiamo una situazione esattamente analoga con l'unione di stringhe letterali, e nessuno è preoccupato per quella situazione. Quindi dubito che sarebbe catastrofico consentire al compilatore di unire tipi identici.)
Quuxplusone

Bene, anche se due funzioni equivalenti (tranne come se) possono essere identiche è una bella domanda. Il linguaggio nello standard non è abbastanza ovvio per le funzioni libere e / o statiche. Ma questo è fuori dallo scopo qui.
Deduplicatore

Casualmente, proprio questo mese c'è stata una discussione sulla mailing list LLVM sulle funzioni di fusione. Il codegen di Clang farà sì che funzioni con corpi completamente vuoti vengano unite quasi "per sbaglio": godbolt.org/z/obT55b Questo è tecnicamente non conforme, e penso che probabilmente correggeranno LLVM per smettere di farlo. Ma sì, d'accordo, anche l'unione degli indirizzi delle funzioni è una cosa.
Quuxplusone

Questo esempio ha altri problemi, vale a dire la mancanza dell'istruzione return. Da soli non rendono già il codice non conforme? Inoltre, cercherò la discussione, ma hanno mostrato o presunto che l'unione di funzioni equivalenti non sia conforme allo standard, al loro comportamento documentato, a gcc, o semplicemente che alcuni si affidano al fatto che non accade?
Deduplicatore

9

Le espressioni lambda C ++ richiedono tipi distinti per operazioni distinte, poiché C ++ si associa in modo statico. Sono costruibili solo in copia / spostamento, quindi per lo più non è necessario nominare il loro tipo. Ma è tutto un po 'un dettaglio di implementazione.

Non sono sicuro che le espressioni lambda C # abbiano un tipo, poiché sono "espressioni di funzione anonima" e vengono immediatamente convertite in un tipo di delegato compatibile o in un tipo di albero delle espressioni. In tal caso, è probabilmente un tipo impronunciabile.

C ++ ha anche strutture anonime, in cui ogni definizione porta a un tipo univoco. Qui il nome non è impronunciabile, semplicemente non esiste per quanto riguarda lo standard.

C # ha tipi di dati anonimi , che vieta accuratamente di sfuggire dall'ambito in cui sono definiti. L'implementazione dà anche a quelli un nome univoco e impronunciabile.

Avere un tipo anonimo segnala al programmatore che non dovrebbero curiosare all'interno della loro implementazione.

A parte:

È possibile dare un nome a tipo di un lambda.

auto foo = []{}; 
using Foo_t = decltype(foo);

Se non si dispone di acquisizioni, è possibile utilizzare un tipo di puntatore a funzione

void (*pfoo)() = foo;

1
Il primo codice di esempio ancora non consentirà un successivo Foo_t = []{};, solo Foo_t = fooe nient'altro.
cmaster - reintegrare monica il

1
@ cmaster-reinstatemonica è perché il tipo non è costruibile di default, non per l'anonimato. La mia ipotesi è che abbia a che fare con l'evitare una serie ancora più ampia di casi d'angolo che devi ricordare, come qualsiasi motivo tecnico.
Caleth

6

Perché usare i tipi anonimi?

Per i tipi generati automaticamente dal compilatore, la scelta è di (1) onorare la richiesta dell'utente per il nome del tipo o (2) lasciare che il compilatore ne scelga uno da solo.

  1. Nel primo caso, ci si aspetta che l'utente fornisca esplicitamente un nome ogni volta che appare un tale costrutto (C ++ / Rust: ogni volta che viene definita una lambda; Rust: ogni volta che viene definita una funzione). Questo è un dettaglio noioso che l'utente deve fornire ogni volta e nella maggior parte dei casi non si fa più riferimento al nome. Quindi ha senso lasciare che il compilatore trovi automaticamente un nome e utilizzare funzionalità esistenti come decltypeo inferenza del tipo per fare riferimento al tipo nei pochi punti in cui è necessario.

  2. In quest'ultimo caso, il compilatore deve scegliere un nome univoco per il tipo, che probabilmente sarebbe un nome oscuro e illeggibile come __namespace1_module1_func1_AnonymousFunction042. Il progettista del linguaggio potrebbe specificare precisamente come questo nome è costruito con dettagli gloriosi e delicati, ma questo espone inutilmente all'utente un dettaglio di implementazione su cui nessun utente ragionevole potrebbe fare affidamento, poiché il nome è senza dubbio fragile di fronte a refactors anche minori. Ciò limita anche inutilmente l'evoluzione del linguaggio: future aggiunte di funzionalità potrebbero causare la modifica dell'algoritmo di generazione del nome esistente, portando a problemi di compatibilità con le versioni precedenti. Pertanto, ha senso omettere semplicemente questo dettaglio e affermare che il tipo generato automaticamente non è indicabile dall'utente.

Perché utilizzare tipi unici (distinti)?

Se un valore ha un tipo univoco, un compilatore di ottimizzazione può tenere traccia di un tipo univoco in tutti i suoi siti di utilizzo con fedeltà garantita. Come corollario, l'utente può quindi essere certo dei luoghi in cui la provenienza di questo particolare valore è pienamente nota al compilatore.

Ad esempio, il momento in cui il compilatore vede:

let f: __UniqueFunc042 = || { ... };  // definition of __UniqueFunc042 (assume it has a nontrivial closure)

/* ... intervening code */

let g: __UniqueFunc042 = /* some expression */;
g();

il compilatore ha piena fiducia che gdeve necessariamente provenire da f, senza nemmeno conoscere la provenienza di g. Ciò consentirebbe gdi devirtualizzare la chiamata . L'utente lo saprebbe anche questo, poiché l'utente ha avuto grande cura di preservare il tipo unico di fattraverso il flusso di dati che ha portato a g.

Necessariamente, questo limita ciò che l'utente può fare con f. L'utente non è libero di scrivere:

let q = if some_condition { f } else { || {} };  // ERROR: type mismatch

poiché ciò porterebbe all'unificazione (illegale) di due tipi distinti.

Per ovviare a questo problema, l'utente potrebbe eseguire l'upcast del __UniqueFunc042tipo non univoco &dyn Fn(),

let f2 = &f as &dyn Fn();  // upcast
let q2 = if some_condition { f2 } else { &|| {} };  // OK

Il compromesso fatto da questo tipo di cancellazione è che l'uso di &dyn Fn()complicare il ragionamento per il compilatore. Dato:

let g2: &dyn Fn() = /*expression */;

il compilatore deve esaminare minuziosamente il /*expression */per determinare se ha g2origine da fo da qualche altra funzione (i) e le condizioni in cui tale provenienza è valida. In molte circostanze, il compilatore può arrendersi: forse l'umano potrebbe dire che g2proviene davvero da fin tutte le situazioni, ma il percorso da fa g2era troppo complicato per essere decifrato dal compilatore, risultando in una chiamata virtuale a g2con prestazioni pessimistiche.

Ciò diventa più evidente quando tali oggetti vengono consegnati a funzioni generiche (modello):

fn h<F: Fn()>(f: F);

Se uno chiama h(f)dove f: __UniqueFunc042, hè specializzato in un'istanza unica:

h::<__UniqueFunc042>(f);

Ciò consente al compilatore di generare codice specializzato h, su misura per l'argomento particolare di f, e l'invio a fè molto probabile che sia statico, se non inline.

Nello scenario opposto, in cui si chiama h(f)con f2: &Fn(), hviene istanziato come

h::<&Fn()>(f);

che è condiviso tra tutte le funzioni di tipo &Fn(). Dall'interno h, il compilatore sa molto poco di una funzione opaca di tipo &Fn()e quindi potrebbe chiamare in modo conservativo solo fcon un invio virtuale. Per inviare in modo statico, il compilatore dovrebbe incorporare la chiamata al h::<&Fn()>(f)sito della chiamata, il che non è garantito se hè troppo complesso.


La prima parte sulla scelta dei nomi non ha senso: un tipo come void(*)(int, double)potrebbe non avere un nome, ma posso scriverlo. Lo definirei un tipo senza nome, non un tipo anonimo. E chiamerei cose criptiche come __namespace1_module1_func1_AnonymousFunction042alterazione dei nomi, che non rientra sicuramente nell'ambito di questa domanda. Questa domanda riguarda i tipi che sono garantiti dallo standard come impossibili da scrivere, al contrario dell'introduzione di una sintassi di tipo che può esprimere questi tipi in modo utile.
cmaster - ripristina monica il

3

Innanzitutto, lambda senza acquisizione sono convertibili in un puntatore a funzione. Quindi forniscono una qualche forma di genericità.

Ora, perché i lambda con acquisizione non sono convertibili in puntatore? Poiché la funzione deve accedere allo stato di lambda, questo stato dovrebbe apparire come argomento della funzione.


Ebbene, le acquisizioni dovrebbero diventare parte della lambda stessa, no? Proprio come sono incapsulati in un file std::function<>.
cmaster - ripristina monica il

3

Per evitare collisioni di nomi con il codice utente.

Anche due lambda con la stessa implementazione avranno tipi diversi. Il che va bene perché posso avere tipi diversi anche per gli oggetti anche se il loro layout di memoria è uguale.


Un tipo come int (*)(Foo*, int, double)non corre alcun rischio di collisione del nome con il codice utente.
cmaster - reintegrare monica il

Il tuo esempio non generalizza molto bene. Sebbene un'espressione lambda sia solo sintassi, valuterà qualche struttura, specialmente con la clausola di cattura. Darlo esplicitamente potrebbe portare a conflitti di nome di strutture già esistenti.
Knivil

Ancora una volta, questa domanda riguarda la progettazione del linguaggio, non il C ++. Posso sicuramente definire un linguaggio in cui il tipo di lambda è più simile a un tipo di puntatore a funzione che a un tipo di struttura dati. La sintassi del puntatore alla funzione in C ++ e la sintassi del tipo di matrice dinamica in C dimostrano che ciò è possibile. E questo solleva la domanda, perché lambda non ha utilizzato un approccio simile?
cmaster - ripristina monica il

1
No, non puoi, a causa del currying variabile (cattura). Hai bisogno sia di una funzione che di dati per farlo funzionare.
Blindy

@ Blindy Oh, sì, posso. Potrei definire un lambda come un oggetto contenente due puntatori, uno per l'oggetto di acquisizione e uno per il codice. Un tale oggetto lambda sarebbe facile da trasferire per valore. Oppure potrei tirare trucchi con uno stub di codice all'inizio dell'oggetto di acquisizione che prende il proprio indirizzo prima di saltare al codice lambda effettivo. Ciò trasformerebbe un puntatore lambda in un singolo indirizzo. Ma non è necessario, come ha dimostrato la piattaforma PPC: su PPC, un puntatore a funzione è in realtà una coppia di puntatori. Ecco perché non si può lanciare void(*)(void)per void*e schienale in serie C / C ++.
cmaster - ripristina monica il
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.