Differenza di comportamento dell'acquisizione mutabile della funzione lambda da un riferimento a variabile globale


22

Ho scoperto che i risultati sono diversi tra i compilatori se uso un lambda per acquisire un riferimento a una variabile globale con una parola chiave mutabile e quindi modificare il valore nella funzione lambda.

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

Risultato di VS 2015 e GCC (g ++ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.12) 5.4.0 20160609):

100 223 100

Risultato da clang ++ (clang versione 3.8.0-2ubuntu4 (tag / RELEASE_380 / final)):

100 223 223

Perché succede? Questo è consentito dagli standard C ++?


Il comportamento di Clang è ancora presente sul tronco.
Noce

Queste sono tutte versioni del compilatore piuttosto vecchie
MM

Presenta ancora la recente versione di Clang: godbolt.org/z/P9na9c
Willy

1
Se rimuovi completamente l'acquisizione, GCC accetta ancora questo codice e fa ciò che fa clang. Questo è un forte suggerimento che c'è un bug GCC - le catture semplici non dovrebbero cambiare il significato del corpo lambda.
TC

Risposte:


16

Un lambda non può acquisire un riferimento stesso per valore (utilizzare std::reference_wrappera tale scopo).

Nel tuo lambda, le [m]acquisizioni mper valore (perché non ci sono &nella cattura), quindi m(essendo un riferimento a n) viene prima dereferenziato e viene catturata una copia della cosa a cui fa riferimento ( n). Questo non è diverso dal fare questo:

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

La lambda modifica quindi quella copia, non l'originale. Questo è ciò che stai vedendo accadere nelle uscite VS e GCC, come previsto.

L'output di Clang è errato e dovrebbe essere segnalato come un bug, se non lo è già.

Se si desidera che il lambda da modificare n, cattura mper riferimento invece: [&m]. Ciò non è diverso dall'assegnare un riferimento a un altro, ad esempio:

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

In alternativa, si può semplicemente sbarazzarsi di mtutto e di cattura nper riferimento invece: [&n].

Sebbene, dal momento che ha nuna portata globale, in realtà non ha bisogno di essere catturato, Lambda può accedervi a livello globale senza catturarlo:

return [] () -> int {
    n += 123;
    return n;
};

5

Penso che Clang possa effettivamente essere corretto.

Secondo [lambda.capture] / 11 , un'espressione id usata nella lambda si riferisce al membro acquisito per copia della lambda solo se costituisce un uso dispari . In caso contrario, si riferisce all'entità originale . Questo vale per tutte le versioni di C ++ da C ++ 11.

Secondo [basic.dev.odr] / 3 di C ++ 17, una variabile di riferimento non viene utilizzata in modo dispari se l'applicazione di una conversione da valore in valore produce un'espressione costante.

Nella bozza C ++ 20, tuttavia, il requisito per la conversione da lvalue a rvalue viene eliminato e il passaggio rilevante viene modificato più volte per includere o meno la conversione. Vedi il numero 1472 di CWG e il numero 1741 di CWG , nonché il numero 2083 di CWG aperto .

Poiché mè inizializzato con un'espressione costante (riferita a un oggetto durata della memoria statica), usandolo si ottiene un'espressione costante per eccezione in [expr.const] /2.11.1 .

Questo non è tuttavia il caso in cui vengono applicate le conversioni da lvalue a rvalue, poiché il valore di nnon è utilizzabile in un'espressione costante.

Pertanto, a seconda che si supponga che le conversioni da lvalue a rvalue debbano essere applicate nel determinare l'uso odr, quando si usa min lambda, può o meno riferirsi al membro del lambda.

Se la conversione deve essere applicata, GCC e MSVC sono corretti, altrimenti Clang lo è.

Puoi vedere che Clang cambia comportamento se cambi l'inizializzazione di mnon essere più un'espressione costante:

#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 questo caso tutti i compilatori concordano sul fatto che l'output è

100 223 100

perché min lambda farà riferimento al membro della chiusura di tipo intinizializzato dalla variabile di riferimento min f.


Entrambi i risultati VS / GCC e Clang sono corretti? O solo uno di loro?
Willy,

[basic.dev.odr] / 3 afferma che la variabile mè utilizzata da un'espressione denominata odr a meno che l'applicazione della conversione da lvalue a rvalue non sia un'espressione costante. Con [expr.const] / (2.7), quella conversione non sarebbe un'espressione costante fondamentale.
aschepler il

Se il risultato di Clang è corretto, penso che sia in qualche modo controintuitivo. Perché dal punto di vista del programmatore, deve assicurarsi che la variabile che scrive nell'elenco di acquisizione sia effettivamente copiata per un caso mutabile, e l'inizializzazione di m potrebbe essere modificata dal programmatore in seguito per qualche motivo.
Willy,

1
m += 123;Qui mè usato odr.
Oliv

1
Penso che Clang abbia ragione con l'attuale formulazione, e anche se non ho approfondito questo, i cambiamenti rilevanti qui sono quasi certamente tutti i DR.
TC

4

Ciò non è consentito dalla norma C ++ 17, ma da alcune altre bozze della norma potrebbe essere. È complicato, per ragioni non spiegate in questa risposta.

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

Per ogni entità acquisita dalla copia, un membro di dati non statico senza nome viene dichiarato nel tipo di chiusura. L'ordine di dichiarazione di questi membri non è specificato. Il tipo di un tale membro di dati è il tipo di riferimento se l'entità è un riferimento a un oggetto, un riferimento lvalue al tipo di funzione di riferimento se l'entità è un riferimento a una funzione o il tipo di entità acquisita corrispondente in caso contrario.

I [m]mezzi che la variabile ma fviene catturato da copia. L'entità mè un riferimento all'oggetto, quindi il tipo di chiusura ha un membro il cui tipo è il tipo di riferimento. Cioè, il tipo di membro è inte non int&.

Poiché il nome mall'interno del corpo lambda nomina il membro dell'oggetto di chiusura e non la variabile in f(e questa è la parte discutibile), l'istruzione m += 123;modifica quel membro, che è un intoggetto diverso da ::n.

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.