Warum eine Sprache mit eindeutigen anonymen Typen entwerfen?

90

Dies ist etwas, das mich als Merkmal von C ++ - Lambda-Ausdrücken immer nervt: Der Typ eines C ++ - Lambda-Ausdrucks ist einzigartig und anonym, ich kann ihn einfach nicht aufschreiben. Selbst wenn ich zwei Lambdas erstelle, die syntaktisch genau gleich sind, werden die resultierenden Typen als unterschiedlich definiert. Die Folge ist, dass a) Lambdas nur an Vorlagenfunktionen übergeben werden können, mit denen die Kompilierungszeit, der unaussprechliche Typ zusammen mit dem Objekt übergeben werden kann, und b) dass Lambdas nur dann nützlich sind, wenn sie über gelöscht werden std::function<>.

Ok, aber so macht es C ++, ich war bereit, es als lästiges Feature dieser Sprache abzuschreiben. Ich habe jedoch gerade erfahren, dass Rust anscheinend dasselbe tut: Jede Rust-Funktion oder jedes Lambda hat einen eindeutigen, anonymen Typ. Und jetzt frage ich mich: Warum?

Meine Frage lautet also:
Was ist aus Sicht des Sprachdesigners der Vorteil, das Konzept eines einzigartigen, anonymen Typs in eine Sprache einzuführen?

cmaster - Monica wieder einsetzen
quelle
6
Wie immer ist die bessere Frage, warum nicht.
Stargateur
31
"dass Lambdas nur dann nützlich sind, wenn sie über std :: function <> vom Typ gelöscht wurden" - nein, ohne sind sie direkt nützlich std::function. Ein Lambda, das an eine Vorlagenfunktion übergeben wurde, kann direkt ohne Beteiligung aufgerufen werden std::function. Der Compiler kann dann das Lambda in die Vorlagenfunktion einbinden, wodurch die Laufzeiteffizienz verbessert wird.
Erlkoenig
1
Ich vermute, es erleichtert die Implementierung von Lambda und erleichtert das Verständnis der Sprache. Wenn Sie zugelassen hätten, dass genau derselbe Lambda-Ausdruck in denselben Typ gefaltet wird, benötigen Sie spezielle Regeln, { int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }da die Variable, auf die er sich bezieht, unterschiedlich ist, obwohl sie textlich identisch sind. Wenn Sie nur sagen, dass sie alle einzigartig sind, müssen Sie sich keine Sorgen machen, um es herauszufinden.
NathanOliver
5
Sie können aber auch einem Lambdas-Typ einen Namen geben und das Gleiche tun. lambdas_type = decltype( my_lambda);
idclev 463035818
3
Aber was sollte eine Art generisches Lambda sein [](auto) {}? Sollte es zunächst einen Typ haben?
Evg

Antworten:

78

Viele Standards (insbesondere C ++) verfolgen den Ansatz, die Anforderungen an Compiler zu minimieren. Ehrlich gesagt verlangen sie schon genug! Wenn sie nichts angeben müssen, damit es funktioniert, neigen sie dazu, die Implementierung definiert zu lassen.

Wären Lambdas nicht anonym, müssten wir sie definieren. Dies muss viel darüber aussagen, wie Variablen erfasst werden. Betrachten Sie den Fall eines Lambda [=](){...}. Der Typ müsste angeben, welche Typen tatsächlich vom Lambda erfasst wurden, was nicht trivial zu bestimmen sein könnte. Was ist auch, wenn der Compiler eine Variable erfolgreich optimiert? Erwägen:

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

Ein optimierender Compiler könnte leicht erkennen, dass der einzig mögliche Wert idavon 5 ist, und diesen durch ersetzen auto f = []() { return 5; }. Wenn der Typ jedoch nicht anonym ist, kann dies den Typ ändern oder den Compiler dazu zwingen, weniger zu optimieren und zu speichern i, obwohl er ihn nicht wirklich benötigt. Dies ist eine ganze Menge Komplexität und Nuancen, die für das, was Lambdas tun sollten, einfach nicht benötigt werden.

Und für den Fall, dass Sie tatsächlich einen nicht anonymen Typ benötigen, können Sie die Abschlussklasse jederzeit selbst erstellen und mit einem Funktor anstelle einer Lambda-Funktion arbeiten. Auf diese Weise können Lambdas den 99% -Fall bearbeiten und Sie können Ihre eigene Lösung in 1% codieren.


Deduplicator wies in Kommentaren darauf hin, dass ich die Einzigartigkeit weniger ansprach als die Anonymität. Ich bin mir der Vorteile der Einzigartigkeit weniger sicher, aber es ist erwähnenswert, dass das folgende Verhalten klar ist, wenn die Typen eindeutig sind (die Aktion wird zweimal instanziiert).

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

Wenn die Typen nicht eindeutig wären, müssten wir angeben, welches Verhalten in diesem Fall auftreten soll. Das könnte schwierig sein. Einige der Fragen, die zum Thema Anonymität aufgeworfen wurden, erheben in diesem Fall auch ihren hässlichen Kopf für die Einzigartigkeit.

Cort Ammon
quelle
Beachten Sie, dass es hier nicht wirklich darum geht, Arbeit für einen Compiler-Implementierer zu speichern, sondern darum, Arbeit für den Standardbetreuer zu speichern. Der Compiler muss noch alle oben genannten Fragen für seine spezifische Implementierung beantworten, sie sind jedoch nicht im Standard angegeben.
ComicSansMS
2
@ComicSansMS Das Zusammenstellen solcher Dinge bei der Implementierung eines Compilers ist viel einfacher, wenn Sie Ihre Implementierung nicht an den Standard eines anderen anpassen müssen. Erfahrungsgemäß ist es für einen Standardbetreuer oft viel einfacher, Funktionen zu spezifizieren, als zu versuchen, die zu spezifizierende Mindestmenge zu finden, während die gewünschte Funktionalität aus Ihrer Sprache herauskommt. Sehen Sie sich als hervorragende Fallstudie an, wie viel Arbeit sie aufgewendet haben, um zu vermeiden, dass memory_order_consume überbestimmt wird, und machen Sie es dennoch nützlich (auf einigen Architekturen)
Cort Ammon
1
Wie alle anderen machen Sie ein überzeugendes Argument für anonym . Aber ist es wirklich eine so gute Idee, es auch zu zwingen , einzigartig zu sein ?
Deduplikator
Hier kommt es nicht auf die Komplexität des Compilers an, sondern auf die Komplexität des generierten Codes. Es geht nicht darum, den Compiler zu vereinfachen, sondern ihm genügend Spielraum zu geben, um alle Fälle zu optimieren und natürlichen Code für die Zielplattform zu erstellen.
Jan Hudec
Sie können keine statische Variable erfassen.
Ruslan
70

Lambdas sind nicht nur Funktionen, sie sind eine Funktion und ein Zustand . Daher implementieren sowohl C ++ als auch Rust sie als Objekt mit einem Aufrufoperator ( operator()in C ++ die 3 Fn*Merkmale in Rust).

Grundsätzlich [a] { return a + 1; }in C ++ Desugars zu so etwas wie

struct __SomeName {
    int a;

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

Verwenden Sie dann eine Instanz, in der __SomeNamedas Lambda verwendet wird.

Während in Rust, wird || a + 1in Rust zu so etwas desugar

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

Dies bedeutet, dass die meisten Lambdas unterschiedliche Arten haben müssen .

Nun gibt es einige Möglichkeiten, wie wir das tun können:

  • Mit anonymen Typen, was beide Sprachen implementieren. Eine weitere Folge davon ist , dass alle Lambdas muss eine andere Art haben. Für Sprachdesigner hat dies jedoch einen klaren Vorteil: Lambdas können einfach mit anderen bereits vorhandenen einfacheren Teilen der Sprache beschrieben werden. Sie sind nur Syntaxzucker um bereits vorhandene Teile der Sprache.
  • Mit einer speziellen Syntax zum Benennen von Lambda-Typen: Dies ist jedoch nicht erforderlich, da Lambdas bereits mit Vorlagen in C ++ oder mit Generika und den Fn*Merkmalen in Rust verwendet werden können. Keine der beiden Sprachen zwingt Sie jemals dazu, Lambdas zu löschen, um sie zu verwenden (mit std::functionin C ++ oder Box<Fn*>in Rust).

Beachten Sie auch , dass beide Sprachen nicht darüber einig , dass trivial Lambdas , die nicht capture Kontext tun können zu Funktionszeiger umgewandelt werden.


Das Beschreiben komplexer Funktionen einer Sprache mit einfacheren Funktionen ist weit verbreitet. Zum Beispiel haben sowohl C ++ als auch Rust Range-for-Schleifen und beide beschreiben sie als Syntaxzucker für andere Funktionen.

C ++ definiert

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

als äquivalent zu

{

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

        range_declaration = *__begin;
        loop_statement

    }

} 

und Rust definiert

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

als äquivalent zu

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

Die für einen Menschen komplizierter erscheinen, für einen Sprachdesigner oder einen Compiler jedoch einfacher sind.

mcarton
quelle
15
@ cmaster-reinstatemonica Überlegen Sie, ob Sie ein Lambda als Vergleichsargument für eine Sortierfunktion übergeben möchten. Möchten Sie hier wirklich einen Overhead für virtuelle Funktionsaufrufe auferlegen?
Daniel Langr
5
@ cmaster-reinstatemonica, da in C ++ standardmäßig nichts virtuell ist
Caleth
4
@cmaster - Sie meinen, alle Benutzer von Lambdas zu zwingen, für dynamisches Dipatch zu bezahlen, auch wenn sie es nicht benötigen?
StoryTeller - Unslander Monica
4
@ cmaster-reinstatemonica Das Beste, was Sie bekommen, ist die Anmeldung für virtuell. std::function
Ratet
9
@ cmaster-reinstatemonica Jeder Mechanismus, bei dem Sie die aufzurufende Funktion neu verweisen können, hat Situationen mit Laufzeit-Overhead. Das ist nicht der C ++ Weg. Sie entscheiden sich fürstd::function
Caleth
13

(Hinzufügen zu Caleths Antwort, aber zu lang, um in einen Kommentar zu passen.)

Der Lambda-Ausdruck ist nur syntaktischer Zucker für eine anonyme Struktur (ein Voldemort-Typ, da Sie seinen Namen nicht sagen können).

Sie können die Ähnlichkeit zwischen einer anonymen Struktur und der Anonymität eines Lambda in diesem Codefragment sehen:

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

Wenn dies für ein Lambda immer noch unbefriedigend ist, sollte es für eine anonyme Struktur ebenfalls unbefriedigend sein.

Einige Sprachen ermöglichen eine etwas flexiblere Art der Ententypisierung, und obwohl C ++ Vorlagen enthält, die nicht wirklich dazu beitragen, ein Objekt aus einer Vorlage zu erstellen, die ein Mitgliedsfeld enthält, das ein Lambda direkt ersetzen kann, anstatt ein zu verwenden std::functionVerpackung.

Eljay
quelle
3
Vielen Dank, das wirft tatsächlich ein wenig Licht auf die Gründe für die Definition von Lambdas in C ++ (ich muss mich an den Begriff "Voldemort-Typ" erinnern :-)). Es bleibt jedoch die Frage: Was ist der Vorteil davon in den Augen eines Sprachdesigners?
cmaster
1
Sie können int& operator()(){ return x; }diese Strukturen sogar ergänzen
Caleth
2
@ cmaster-reinstatemonica • Spekulativ ... verhält sich der Rest von C ++ so. Lambdas dazu zu bringen, eine Art "Oberflächenform" zu verwenden, wäre etwas ganz anderes als der Rest der Sprache. Das Hinzufügen dieser Art von Einrichtung in der Sprache für Lambdas würde wahrscheinlich als verallgemeinert für die gesamte Sprache angesehen, und das wäre eine potenziell große bahnbrechende Änderung. Das Weglassen einer solchen Funktion nur für Lambdas passt zur starken Typisierung des restlichen C ++.
Eljay
Technisch wäre ein Voldemort-Typ auto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }, weil außerhalb von foonichts die Kennung verwendet werden kannDarkLord
Caleth
@ cmaster-reinstatemonica Effizienz, die Alternative wäre, jedes Lambda zu boxen und dynamisch zu versenden (ordne es auf dem Heap zu und lösche seinen genauen Typ). Nun, wie Sie bemerken, könnte der Compiler die anonymen Lambdas-Typen deduplizieren, aber Sie könnten sie immer noch nicht aufschreiben, und es würde erhebliche Arbeit für sehr wenig Gewinn erfordern, so dass die Chancen nicht wirklich günstig sind.
Masklinn
10

Warum eine Sprache mit eindeutigen anonymen Typen entwerfen ?

Weil es Fälle gibt, in denen Namen irrelevant und nicht nützlich oder sogar kontraproduktiv sind. In diesem Fall ist die Fähigkeit, ihre Existenz zu abstrahieren, nützlich, weil sie die Namensverschmutzung verringert und eines der beiden schwierigen Probleme in der Informatik löst (wie man Dinge benennt). Aus dem gleichen Grund sind temporäre Objekte nützlich.

Lambda

Die Einzigartigkeit ist keine besondere Lambda-Sache oder sogar eine besondere Sache für anonyme Typen. Dies gilt auch für benannte Typen in der Sprache. Beachten Sie Folgendes:

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

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

void foo(A);

Beachten Sie, dass ich nicht passieren kann Bin foo, obwohl die Klassen identisch sind. Dieselbe Eigenschaft gilt für unbenannte Typen.

Lambdas können nur an Vorlagenfunktionen übergeben werden, mit denen die Kompilierungszeit, ein unaussprechlicher Typ, zusammen mit dem Objekt übergeben werden kann, das über std :: function <> gelöscht wurde.

Es gibt eine dritte Option für eine Teilmenge von Lambdas: Nicht erfassende Lambdas können in Funktionszeiger konvertiert werden.


Beachten Sie, dass die Lösung einfach ist, wenn die Einschränkungen eines anonymen Typs für einen Anwendungsfall ein Problem darstellen: Stattdessen kann ein benannter Typ verwendet werden. Lambdas tun nichts, was mit einer benannten Klasse nicht möglich ist.

Eerorika
quelle
10

Die akzeptierte Antwort von Cort Ammon ist gut, aber ich denke, es gibt noch einen wichtigen Punkt in Bezug auf die Implementierbarkeit.

Angenommen, ich habe zwei verschiedene Übersetzungseinheiten, "one.cpp" und "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);

Die beiden Überladungen fooverwenden denselben Bezeichner ( foo), haben jedoch unterschiedliche verstümmelte Namen. (In dem Itanium ABI, das auf POSIX-ähnlichen Systemen verwendet wird, sind die verstümmelten Namen _Z3foo1Aund in diesem speziellen Fall _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) {}

Der C ++ - Compiler muss sicherstellen, dass der verstümmelte Name von void foo(A1)in "two.cpp" mit dem verstümmelten Namen von extern void foo(A2)in "one.cpp" übereinstimmt, damit wir die beiden Objektdateien miteinander verknüpfen können. Dies ist die physikalische Bedeutung von zwei Typen, die "der gleiche Typ" sind: Es geht im Wesentlichen um die ABI-Kompatibilität zwischen separat kompilierten Objektdateien.

Der C ++ - Compiler muss dies nicht sicherstellen B1und B2ist "vom gleichen Typ". (Tatsächlich muss sichergestellt werden, dass es sich um verschiedene Typen handelt. Dies ist derzeit jedoch nicht so wichtig.)


Was physikalischer Mechanismus ist an der Compiler Verwendung sicherzustellen , dass A1und A2ist „die gleiche Art“?

Es gräbt sich einfach durch typedefs und betrachtet dann den vollständig qualifizierten Namen des Typs. Es ist ein Klassentyp namens A. (Nun, ::Ada es sich im globalen Namespace befindet.) Es ist also in beiden Fällen der gleiche Typ. Das ist leicht zu verstehen. Noch wichtiger ist, dass es einfach zu implementieren ist . Um festzustellen, ob zwei Klassentypen vom gleichen Typ sind, nehmen Sie ihre Namen und führen a aus strcmp. Um einen Klassentyp in den verstümmelten Namen einer Funktion zu zerlegen, schreiben Sie die Anzahl der Zeichen in den Namen, gefolgt von diesen Zeichen.

So sind benannte Typen leicht zu entstellen.

Was physikalischer Mechanismus könnte der Compiler verwenden , um sicherzustellen , dass B1und B2ist „die gleiche Art“ , in einer hypothetischen Welt , in der C ++ sie benötigte die gleiche Art zu sein?

Nun, es kann nicht den Namen des Typs verwenden, da der Typ nicht funktioniert hat einen Namen.

Vielleicht könnte es irgendwie den Text des Körpers des Lambda verschlüsseln . Aber das wäre etwas umständlich, denn tatsächlich unterscheidet sich das bin "one.cpp" geringfügig von dem bin "two.cpp": "one.cpp" hat x+1und "two.cpp" hat x + 1. Wir müssten uns also eine Regel ausdenken, die besagt, dass entweder dieser Leerzeichenunterschied keine Rolle spielt oder dass dies der Fall ist (was sie schließlich zu unterschiedlichen Typen macht) oder dass dies möglicherweise der Fall ist (möglicherweise ist die Gültigkeit des Programms durch die Implementierung definiert , oder vielleicht ist es "schlecht geformt, keine Diagnose erforderlich"). Wie auch immer,A

Der einfachste Ausweg aus der Schwierigkeit besteht darin, einfach zu sagen, dass jeder Lambda-Ausdruck Werte eines eindeutigen Typs erzeugt. Dann sind zwei Lambda-Typen, die in verschiedenen Übersetzungseinheiten definiert sind, definitiv nicht der gleiche Typ . Innerhalb einer einzelnen Übersetzungseinheit können wir Lambda-Typen "benennen", indem wir nur vom Anfang des Quellcodes an zählen:

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
} 

Natürlich haben diese Namen nur innerhalb dieser Übersetzungseinheit eine Bedeutung. Diese TUs $_0sind immer ein anderer Typ als einige andere TUs $_0, obwohl diese TUs struct Aimmer der gleiche Typ sind wie einige andere TUs struct A.

Beachten Sie übrigens, dass unsere Idee, den Text des Lambda zu kodieren, ein weiteres subtiles Problem hatte: Lambdas $_2und $_3bestehen aus genau demselben Text , aber sie sollten eindeutig nicht als der gleiche Typ betrachtet werden!


By the way, hat C ++ die Compiler erforderlich zu wissen , wie Sie den Text eines beliebigen C ++ mangle Ausdruck , wie in

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

Für C ++ muss der Compiler (noch) nicht wissen, wie eine beliebige C ++ - Anweisung entstellt wird . decltype([](){ ...arbitrary statements... })ist auch in C ++ 20 noch schlecht geformt.


Beachten Sie auch, dass es einfach ist , einem unbenannten Typ mit / einen lokalen Alias ​​zu geben . Ich habe das Gefühl, dass Ihre Frage möglicherweise durch den Versuch entstanden ist, etwas zu tun, das auf diese Weise gelöst werden könnte.typedefusing

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

BEARBEITET ZUM HINZUFÜGEN: Wenn Sie einige Ihrer Kommentare zu anderen Antworten lesen, fragen Sie sich, warum

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

Das liegt daran, dass Captureless Lambdas standardmäßig konstruierbar sind. (In C ++ nur ab C ++ 20, aber konzeptionell war es immer wahr.)

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

Wenn Sie es versuchen würden default_construct_and_call<decltype(&add1)>, twäre dies ein standardmäßig initialisierter Funktionszeiger, und Sie würden wahrscheinlich einen Segfault ausführen. Das ist nicht nützlich.

Quuxpluson
quelle
Tatsächlich muss sichergestellt werden, dass es sich um verschiedene Typen handelt, aber das ist momentan nicht so wichtig. “ Ich frage mich, ob es einen guten Grund gibt, die Eindeutigkeit zu erzwingen, wenn sie gleichwertig definiert ist.
Deduplikator
Persönlich denke ich, dass vollständig definiertes Verhalten (fast?) Immer besser ist als nicht spezifiziertes Verhalten. "Sind diese beiden Funktionszeiger gleich? Nun, nur wenn diese beiden Vorlageninstanziierungen dieselbe Funktion sind. Dies gilt nur, wenn diese beiden Lambda-Typen denselben Typ haben. Dies gilt nur, wenn der Compiler beschlossen hat, sie zusammenzuführen." Eklig! (Beachten Sie jedoch, dass wir eine genau analoge Situation mit dem Zusammenführen von Zeichenfolgenliteralen haben und niemand über diese Situation
beunruhigt ist
Nun, ob zwei äquivalente Funktionen (außer als ob) identisch sein könnten, ist auch eine schöne Frage. Die Sprache im Standard ist für freie und / oder statische Funktionen nicht ganz offensichtlich. Aber das liegt hier außerhalb des Rahmens.
Deduplikator
Zufälligerweise wurde in diesem Monat auf der LLVM-Mailingliste über das Zusammenführen von Funktionen diskutiert . Clangs Codegen wird dazu führen, dass Funktionen mit vollständig leeren Körpern fast "zufällig" zusammengeführt werden: godbolt.org/z/obT55b Dies ist technisch nicht konform, und ich denke, sie werden wahrscheinlich LLVM patchen, um dies zu beenden. Aber ja, vereinbart, das Zusammenführen von Funktionsadressen ist auch eine Sache.
Quuxplusone
Dieses Beispiel hat andere Probleme, nämlich die fehlende return-Anweisung. Machen sie den Code nicht schon alleine nicht konform? Ich werde auch nach der Diskussion suchen, aber haben sie gezeigt oder angenommen, dass das Zusammenführen äquivalenter Funktionen nicht dem Standard, ihrem dokumentierten Verhalten, gcc entspricht oder nur, dass einige sich darauf verlassen, dass dies nicht geschieht?
Deduplikator
9

C ++ - Lambdas benötigen unterschiedliche Typen für unterschiedliche Operationen, da C ++ statisch bindet. Sie können nur kopiert / verschoben werden, sodass Sie ihren Typ meistens nicht benennen müssen. Aber das ist alles ein Implementierungsdetail.

Ich bin nicht sicher, ob C # -Lambdas einen Typ haben, da es sich um "anonyme Funktionsausdrücke" handelt, und sie werden sofort in einen kompatiblen Delegatentyp oder Ausdrucksbaumtyp konvertiert. Wenn dies der Fall ist, handelt es sich wahrscheinlich um einen unaussprechlichen Typ.

C ++ hat auch anonyme Strukturen, wobei jede Definition zu einem eindeutigen Typ führt. Hier ist der Name nicht unaussprechlich, er existiert einfach nicht, was den Standard betrifft.

C # verfügt über anonyme Datentypen , die es sorgfältig verhindern, aus dem von ihnen definierten Bereich zu entkommen. Die Implementierung gibt auch diesen einen eindeutigen, unaussprechlichen Namen.

Ein anonymer Typ signalisiert dem Programmierer, dass er nicht in seiner Implementierung herumstöbern sollte.

Beiseite:

Du kannst einem Lambda-Typ einen Namen geben.

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

Wenn Sie keine Captures haben, können Sie einen Funktionszeigertyp verwenden

void (*pfoo)() = foo;
Caleth
quelle
1
Der erste Beispielcode erlaubt immer noch keine nachfolgende Foo_t = []{};, nur Foo_t = foound sonst nichts.
cmaster
1
@ cmaster-reinstatemonica Das liegt daran, dass der Typ nicht standardmäßig konstruierbar ist, nicht an der Anonymität. Ich vermute, das hat genauso viel damit zu tun, zu vermeiden, dass es noch größere Eckfälle gibt, an die Sie sich erinnern müssen, als aus irgendeinem technischen Grund.
Caleth
6

Warum anonyme Typen verwenden?

Bei Typen, die vom Compiler automatisch generiert werden, können Sie entweder (1) die Anforderung eines Benutzers nach dem Namen des Typs berücksichtigen oder (2) den Compiler einen eigenen auswählen lassen.

  1. Im ersteren Fall wird vom Benutzer erwartet, dass er jedes Mal, wenn ein solches Konstrukt angezeigt wird, explizit einen Namen angibt (C ++ / Rust: wann immer ein Lambda definiert ist; Rust: wann immer eine Funktion definiert ist). Dies ist ein mühsames Detail, das der Benutzer jedes Mal angeben muss, und in den meisten Fällen wird der Name nie wieder erwähnt. Daher ist es sinnvoll, den Compiler automatisch einen Namen für ihn ermitteln zu lassen und vorhandene Funktionen wie decltypeoder Typinferenz zu verwenden, um den Typ an den wenigen Stellen zu referenzieren, an denen er benötigt wird.

  2. Im letzteren Fall muss der Compiler einen eindeutigen Namen für den Typ auswählen, bei dem es sich wahrscheinlich um einen unklaren, unlesbaren Namen handelt, z __namespace1_module1_func1_AnonymousFunction042. Der Sprachdesigner könnte genau angeben, wie dieser Name in prächtigen und feinen Details aufgebaut ist, aber dies legt dem Benutzer unnötig ein Implementierungsdetail offen, auf das sich kein vernünftiger Benutzer verlassen kann, da der Name selbst bei geringfügigen Refaktoren zweifellos spröde ist. Dies schränkt auch die Entwicklung der Sprache unnötig ein: Zukünftige Funktionserweiterungen können dazu führen, dass sich der vorhandene Algorithmus zur Namensgenerierung ändert, was zu Abwärtskompatibilitätsproblemen führt. Daher ist es sinnvoll, dieses Detail einfach wegzulassen und zu behaupten, dass der automatisch generierte Typ für den Benutzer nicht aussprechbar ist.

Warum eindeutige (unterschiedliche) Typen verwenden?

Wenn ein Wert einen eindeutigen Typ hat, kann ein optimierender Compiler einen eindeutigen Typ über alle Verwendungsseiten hinweg mit garantierter Genauigkeit verfolgen. Als Konsequenz kann der Benutzer dann sicher sein, an welchen Stellen die Herkunft dieses bestimmten Werts dem Compiler vollständig bekannt ist.

In dem Moment, in dem der Compiler Folgendes sieht:

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

/* ... intervening code */

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

Der Compiler hat das volle Vertrauen, gdas unbedingt entstehen muss f, ohne die Herkunft von zu kennen g. Dies würde es ermöglichen, den Anruf gzu devirtualisieren. Der Benutzer würde dies auch wissen, da der Benutzer sehr darauf geachtet hat, den einzigartigen Typ des fdurch den Datenfluss, der dazu führte, beizubehalteng .

Dies schränkt notwendigerweise ein, was der Benutzer tun kann f. Dem Benutzer steht es nicht frei zu schreiben:

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

da dies zur (illegalen) Vereinigung zweier unterschiedlicher Typen führen würde.

Um dies zu umgehen, kann der Benutzer den __UniqueFunc042auf den nicht eindeutigen Typ übertragen &dyn Fn().

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

Der Kompromiss, den diese Art des Löschens eingeht, besteht darin, dass Verwendungen &dyn Fn()die Argumentation für den Compiler erschweren. Gegeben:

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

Der Compiler muss das sorgfältig prüfen, /*expression */um festzustellen, ob es g2von einer foder mehreren anderen Funktionen stammt und unter welchen Bedingungen diese Herkunft gilt. Unter vielen Umständen kann der Compiler aufgeben: Vielleicht kann der Mensch erkennen, dass dies g2wirklich fin allen Situationen der Fall ist, aber der Pfad von fzu g2war zu kompliziert, als dass der Compiler ihn entschlüsseln könnte, was zu einem virtuellen Aufruf von führteg2 mit pessimistischer Leistung führte.

Dies wird deutlicher, wenn solche Objekte an generische (Vorlagen-) Funktionen geliefert werden:

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

Wenn man h(f)wo anruft f: __UniqueFunc042, hist man auf eine eindeutige Instanz spezialisiert:

h::<__UniqueFunc042>(f);

Auf diese Weise kann der Compiler speziellen Code für hdas jeweilige Argument fund den Versand an generierenf ist höchstwahrscheinlich statisch, wenn nicht inline.

Im umgekehrten Fall, wo man Anrufe h(f)mit f2: &Fn(), die hinstanziiert als

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

welches von allen Funktionen des Typs geteilt wird &Fn(). Von innen hweiß der Compiler sehr wenig über eine undurchsichtige Funktion des Typs &Fn()und konnte daher nur konservativ fmit einem virtuellen Versand aufrufen . Um statisch zu versenden, müsste der Compiler den Aufruf h::<&Fn()>(f)an seiner Aufrufstelle einbinden, was nicht garantiert werden kann, wenn er hzu komplex ist.

Rüschenwind
quelle
Der erste Teil über die Auswahl von Namen geht am eigentlichen Punkt vorbei: Ein Typ wie hat void(*)(int, double)möglicherweise keinen Namen, aber ich kann ihn aufschreiben. Ich würde es einen namenlosen Typ nennen, keinen anonymen Typ. Und ich würde kryptisches Zeug wie __namespace1_module1_func1_AnonymousFunction042Name Mangling nennen, was definitiv nicht im Rahmen dieser Frage liegt. Bei dieser Frage geht es um Typen, von denen der Standard garantiert, dass sie nicht aufgeschrieben werden können, anstatt eine Typensyntax einzuführen, die diese Typen auf nützliche Weise ausdrücken kann.
cmaster
3

Erstens kann Lambda ohne Erfassung in einen Funktionszeiger konvertiert werden. Sie bieten also irgendeine Form von Großzügigkeit.

Warum können Lambdas mit Capture nicht in Zeiger konvertiert werden? Da die Funktion auf den Status des Lambda zugreifen muss, muss dieser Status als Funktionsargument angezeigt werden.

Oliv
quelle
Nun, die Aufnahmen sollten Teil des Lambda selbst werden, nicht wahr? Genau wie sie in einem eingekapselt sind std::function<>.
cmaster
3

Um Namenskollisionen mit dem Benutzercode zu vermeiden.

Sogar zwei Lambdas mit derselben Implementierung haben unterschiedliche Typen. Was in Ordnung ist, weil ich auch für Objekte verschiedene Typen haben kann, selbst wenn deren Speicherlayout gleich ist.

knivil
quelle
Bei einem Typ wie int (*)(Foo*, int, double)besteht kein Risiko einer Namenskollision mit dem Benutzercode.
cmaster
Ihr Beispiel lässt sich nicht sehr gut verallgemeinern. Während ein Lambda-Ausdruck nur eine Syntax ist, wird er insbesondere mit der Capture-Klausel zu einer bestimmten Struktur ausgewertet. Eine explizite Benennung kann zu Namenskonflikten bereits vorhandener Strukturen führen.
Knivil
Auch bei dieser Frage geht es um Sprachdesign, nicht um C ++. Ich kann sicherlich eine Sprache definieren, in der der Typ eines Lambdas eher einem Funktionszeigertyp als einem Datenstrukturtyp ähnelt. Die Funktionszeigersyntax in C ++ und die dynamische Array-Typsyntax in C beweisen, dass dies möglich ist. Und das wirft die Frage auf, warum Lambdas keinen ähnlichen Ansatz gewählt haben.
cmaster
1
Nein, das kannst du nicht, weil du variabel curryst (erfassen) kannst. Sie benötigen sowohl eine Funktion als auch Daten, damit es funktioniert.
Blindy
@Blindy Oh ja, ich kann. Ich könnte ein Lambda als ein Objekt definieren, das zwei Zeiger enthält, einen für das Erfassungsobjekt und einen für den Code. Ein solches Lambda-Objekt wäre leicht wertmäßig weiterzugeben. Oder ich könnte Tricks mit einem Codestummel am Anfang des Erfassungsobjekts ausführen, das seine eigene Adresse annimmt, bevor ich zum eigentlichen Lambda-Code springe. Das würde einen Lambda-Zeiger in eine einzelne Adresse verwandeln. Dies ist jedoch nicht erforderlich, wie die PPC-Plattform bewiesen hat: Bei PPC ist ein Funktionszeiger tatsächlich ein Paar von Zeigern. Aus diesem Grund können Sie in Standard-C / C ++ nicht void(*)(void)in void*und zurück konvertieren.
cmaster