Verhaltensunterschied der veränderlichen Erfassung der Lambda-Funktion von einem Verweis auf eine globale Variable

22

Ich habe festgestellt, dass die Ergebnisse bei Compilern unterschiedlich sind, wenn ich ein Lambda verwende, um einen Verweis auf eine globale Variable mit einem veränderlichen Schlüsselwort zu erfassen und dann den Wert in der Lambda-Funktion zu ändern.

#include <stdio.h>
#include <functional>

int n = 100;

std::function<int()> f()
{
    int &m = n;
    return [m] () mutable -> int {
        m += 123;
        return m;
    };
}

int main()
{
    int x = n;
    int y = f()();
    int z = n;

    printf("%d %d %d\n", x, y, z);
    return 0;
}

Ergebnis von VS 2015 und GCC (g ++ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.12) 5.4.0 20160609):

100 223 100

Ergebnis von clang ++ (clang Version 3.8.0-2ubuntu4 (tags / RELEASE_380 / final)):

100 223 223

Warum passiert das? Ist dies nach den C ++ - Standards zulässig?

Willy
quelle
Clangs Verhalten ist immer noch am Kofferraum vorhanden.
Walnuss
Dies sind alles ziemlich alte Compiler-Versionen
MM
Es wird immer noch auf der neuesten Version von Clang präsentiert: godbolt.org/z/P9na9c
Willy
1
Wenn Sie die Erfassung vollständig entfernen, akzeptiert GCC diesen Code weiterhin und führt das aus, was clang tut. Das ist ein starker Hinweis darauf, dass es einen GCC-Fehler gibt - einfache Erfassungen sollen die Bedeutung des Lambda-Körpers nicht ändern.
TC

Antworten:

16

Ein Lambda kann eine Referenz selbst nicht nach Wert erfassen ( std::reference_wrapperfür diesen Zweck verwenden).

In Ihrem Lambda werden [m]Erfassungen mnach Wert erfasst (da &die Erfassung keine enthält ). Daher wird m(als Verweis auf n) zuerst dereferenziert und eine Kopie des Objekts, auf das es verweist ( n), erfasst. Dies ist nicht anders als dies:

int &m = n;
int x = m; // <-- copy made!

Das Lambda ändert dann diese Kopie, nicht das Original. Dies geschieht erwartungsgemäß in den VS- und GCC-Ausgängen.

Die Clang-Ausgabe ist falsch und sollte als Fehler gemeldet werden, falls dies noch nicht geschehen ist.

Wenn Sie möchten, dass Ihr Lambda geändert wird n, erfassen Sie mstattdessen anhand der Referenz : [&m]. Dies unterscheidet sich nicht von der Zuweisung einer Referenz zu einer anderen, z.

int &m = n;
int &x = m; // <-- no copy made!

Oder Sie können es einfach mganz loswerden und nstattdessen als Referenz erfassen : [&n].

Obwohl nes sich im globalen Bereich befindet, muss es überhaupt nicht erfasst werden, aber das Lambda kann global darauf zugreifen, ohne es zu erfassen:

return [] () -> int {
    n += 123;
    return n;
};
Remy Lebeau
quelle
5

Ich denke, Clang kann tatsächlich richtig sein.

Nach [lambda.capture] / 11 , eine ID-Expression in dem Lambda verwendete , bezieht sich auf das von kopier erfaßt Mitglied lambda nur , wenn es einen ausmacht ODR-use . Wenn dies nicht der Fall ist, bezieht es sich auf die ursprüngliche Entität . Dies gilt für alle C ++ - Versionen seit C ++ 11.

Gemäß C ++ 17s [basic.dev.odr] / 3 wird eine Referenzvariable nicht odr-verwendet, wenn die Konvertierung von lWert zu rWert einen konstanten Ausdruck ergibt.

Im C ++ 20-Entwurf entfällt jedoch die Anforderung für die Konvertierung von Wert zu Wert, und die entsprechende Passage wurde mehrmals geändert, um die Konvertierung einzuschließen oder nicht einzuschließen. Siehe CWG-Ausgabe 1472 und CWG-Ausgabe 1741 sowie offene CWG-Ausgabe 2083 .

Da mes mit einem konstanten Ausdruck initialisiert wird (der sich auf ein Objekt mit statischer Speicherdauer bezieht), ergibt die Verwendung einen konstanten Ausdruck pro Ausnahme in [expr.const] /2.11.1 .

Dies ist jedoch nicht der Fall, wenn l-Wert-zu-Wert-Konvertierungen angewendet werden, da der Wert von nin einem konstanten Ausdruck nicht verwendet werden kann.

Abhängig davon, ob bei der Bestimmung der odr-Verwendung Umrechnungen von lwert zu rwert angewendet werden sollen oder nicht, kann sich dies bei Verwendung mim Lambda auf das Mitglied des Lambda beziehen oder nicht.

Wenn die Konvertierung angewendet werden soll, sind GCC und MSVC korrekt, andernfalls ist Clang korrekt.

Sie können sehen, dass Clang sein Verhalten ändert, wenn Sie die Initialisierung so ändern m, dass sie kein konstanter Ausdruck mehr ist:

#include <stdio.h>
#include <functional>

int n = 100;

void g() {}

std::function<int()> f()
{
    int &m = (g(), n);
    return [m] () mutable -> int {
        m += 123;
        return m;
    };
}

int main()
{
    int x = n;
    int y = f()();
    int z = n;

    printf("%d %d %d\n", x, y, z);
    return 0;
}

In diesem Fall stimmen alle Compiler darin überein, dass die Ausgabe ist

100 223 100

denn mim Lambda wird auf das Mitglied intdes Abschlusses verwiesen, das vom Typ kopiert ist, der aus der Referenzvariablen min kopiert wurde f.

Nussbaum
quelle
Sind beide VS / GCC- und Clang-Ergebnisse korrekt? Oder nur einer von ihnen?
Willy
[basic.dev.odr] / 3 besagt, dass die Variable mvon einem Ausdruck, der sie benennt, odr-verwendet wird, es sei denn, die Konvertierung von lvalue in rvalue wäre ein konstanter Ausdruck. Nach [expr.const] / (2.7) wäre diese Konvertierung kein konstanter Kernausdruck.
Aschepler
Wenn Clangs Ergebnis korrekt ist, denke ich, dass es irgendwie nicht intuitiv ist. Aus Sicht des Programmierers muss er sicherstellen, dass die Variable, die er in die Erfassungsliste schreibt, tatsächlich für einen veränderlichen Fall kopiert wird, und die Initialisierung von m kann vom Programmierer aus irgendeinem Grund später geändert werden.
Willy
1
m += 123;Hier mwird odr verwendet.
Oliv
1
Ich denke, Clang hat mit dem aktuellen Wortlaut Recht, und obwohl ich mich nicht damit befasst habe, sind die relevanten Änderungen hier mit ziemlicher Sicherheit alle DRs.
TC
4

Dies ist im C ++ 17-Standard nicht zulässig, in einigen anderen Standardentwürfen jedoch möglicherweise. Es ist aus Gründen, die in dieser Antwort nicht erläutert werden, kompliziert.

[expr.prim.lambda.capture] / 10 :

Für jede durch Kopie erfasste Entität wird ein unbenanntes nicht statisches Datenelement im Schließungstyp deklariert. Die Deklarationsreihenfolge dieser Mitglieder ist nicht festgelegt. Der Typ eines solchen Datenelements ist der referenzierte Typ, wenn die Entität eine Referenz auf ein Objekt ist, eine Wertreferenz auf den referenzierten Funktionstyp, wenn die Entität eine Referenz auf eine Funktion ist, oder der Typ der entsprechenden erfassten Entität auf andere Weise.

Dies [m]bedeutet, dass die Variable min fdurch Kopieren erfasst wird. Die Entität mist eine Referenz auf ein Objekt, daher hat der Schließungstyp ein Element, dessen Typ der referenzierte Typ ist. Das heißt, der Typ des Mitglieds ist intund nicht int&.

Da der Name mim Lambda-Body das Element des Abschlussobjekts und nicht die Variable in f(und dies ist der fragliche Teil) benennt , m += 123;ändert die Anweisung dieses Element, das ein anderes intObjekt ist als ::n.

aschepler
quelle