Różnica w zachowaniu zmiennego wychwytywania funkcji lambda z odniesienia do zmiennej globalnej

22

Stwierdziłem, że wyniki są różne w różnych kompilatorach, jeśli użyję lambda do przechwycenia odwołania do zmiennej globalnej za pomocą słowa kluczowego podlegającego modyfikacji, a następnie zmodyfikowania wartości w funkcji 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;
}

Wynik z VS 2015 i GCC (g ++ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.12) 5.4.0 20160609):

100 223 100

Wynik z clang ++ (wersja clang 3.8.0-2ubuntu4 (tagi / RELEASE_380 / wersja ostateczna)):

100 223 223

Dlaczego to się dzieje? Czy jest to dozwolone przez standardy C ++?

Willy
źródło
Zachowanie Clanga jest nadal obecne na pniu.
orzech
To wszystkie dość stare wersje kompilatora
MM
Nadal prezentuje najnowszą wersję Clanga: godbolt.org/z/P9na9c
Willy
1
Jeśli całkowicie usuniesz przechwytywanie, GCC nadal akceptuje ten kod i robi to, co robi clang. To mocna wskazówka, że ​​występuje błąd GCC - proste przechwytywanie nie powinno zmieniać znaczenia ciała lambda.
TC

Odpowiedzi:

16

Sonda lambda nie może przechwycić samego odwołania według wartości (użyj std::reference_wrapperdo tego celu).

W twojej lambda, [m]przechwytywanie mwedług wartości (ponieważ nie ma &przechwytywania), więc m(jako odniesienie n) jest najpierw usuwane z odniesienia i przechwytywana jest kopia rzeczy, do której się odnosi ( n). Nie różni się to od robienia tego:

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

Następnie lambda modyfikuje tę kopię, a nie oryginał. Tak właśnie się dzieje, zgodnie z oczekiwaniami, na wyjściach VS i GCC.

Dane wyjściowe Clanga są nieprawidłowe i powinny zostać zgłoszone jako błąd, jeśli jeszcze tego nie zrobił.

Jeśli chcesz, aby lambda do modyfikowania n, przechwytywanie mprzez odniesienie zamiast: [&m]. Nie różni się to niczym od przypisania jednego odwołania do drugiego, np .:

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

Albo może po prostu pozbyć się mcałkowicie i uchwycenia nprzez odniesienie zamiast: [&n].

Chociaż skoro nma zasięg globalny, naprawdę nie musi być wcale przechwytywany, lambda może uzyskać do niego dostęp globalny bez przechwytywania:

return [] () -> int {
    n += 123;
    return n;
};
Remy Lebeau
źródło
5

Myślę, że Clang może mieć rację.

Według [lambda.capture] / 11 , wyrażenie id użyte w lambda odnosi się do elementu przechwyconego przez lambda po kopii, tylko jeśli stanowi użycie nieprzydatne . Jeśli nie, oznacza to pierwotny byt . Dotyczy to wszystkich wersji C ++ od C ++ 11.

Według [ ++. dev.odr] / 3 C ++ 17 zmienna referencyjna nie jest używana odr, jeśli zastosowanie do niej konwersji wartości z wartości na wartość daje stałe wyrażenie.

W wersji roboczej C ++ 20 jednak wymóg dotyczący konwersji wartości z wartości na wartość jest odrzucany, a odpowiedni fragment wielokrotnie zmieniany, aby uwzględnić konwersję lub nie. Patrz wydanie CWG 1472 i wydanie CWG 1741 , a także wydanie otwarte CWG 2083 .

Ponieważ mjest inicjowany stałym wyrażeniem (odnoszącym się do obiektu o czasie trwania przechowywania statycznego), użycie go daje stałe wyrażenie na wyjątek w [expr.const] /2.11.1 .

Nie dzieje się tak jednak w przypadku zastosowania konwersji wartości z wartości na wartość, ponieważ wartość parametru nnie jest użyta w wyrażeniu stałym.

W związku z tym, w zależności od tego, czy konwersje wartości z wartości na wartość mają być stosowane przy określaniu użycia odry, podczas korzystania mz lambda może, ale nie musi odnosić się do członka lambda.

Jeśli konwersja powinna być zastosowana, GCC i MSVC są poprawne, w przeciwnym razie Clang jest.

Możesz zobaczyć, że Clang zmienia to zachowanie, jeśli zmienisz inicjalizację, maby nie była już stałym wyrażeniem:

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

W tym przypadku wszystkie kompilatory zgadzają się, że dane wyjściowe są

100 223 100

ponieważ mw lambda będzie odnosić się do elementu zamknięcia, który jest typu intinicjalizowany kopiowaniem ze zmiennej odniesienia mw f.

orzech włoski
źródło
Czy oba wyniki VS / GCC i Clang są poprawne? Czy tylko jeden z nich?
Willy
[basic.dev.odr] / 3 mówi, że zmienna mjest używana przez odr przez wyrażenie nazywające ją wyrażenie, chyba że zastosowanie do niej konwersji lvalue na rvalue byłoby ciągłym wyrażeniem. Według [expr.const] / (2.7) ta konwersja nie byłaby podstawowym stałym wyrażeniem.
aschepler
Jeśli wynik Clanga jest prawidłowy, myślę, że jest to w jakiś sposób sprzeczne z intuicją. Ponieważ z punktu widzenia programisty musi on upewnić się, że zmienna, którą zapisuje na liście przechwytywania, jest faktycznie kopiowana w przypadku zmiennych, a inicjacja m może zostać z jakiegoś powodu zmieniona przez programistę później.
Willy
1
m += 123;Oto mużywane odr.
Oliv
1
Myślę, że Clang ma rację w obecnym brzmieniu i chociaż nie wgłębiłem się w to, odpowiednie zmiany tutaj są prawie na pewno wszystkie DR.
TC
4

Nie jest to dozwolone w standardzie C ++ 17, ale może być w niektórych innych wersjach standardowych. Jest to skomplikowane, z powodów nie wyjaśnionych w tej odpowiedzi.

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

Dla każdego obiektu przechwyconego przez kopię deklarowany jest nienazwany element danych niestatycznych w typie zamknięcia. Kolejność deklaracji tych członków jest nieokreślona. Typem takiego elementu danych jest typ odwołania, jeśli encja jest odwołaniem do obiektu, odwołanie do wartości typu odwołania do typu funkcji, jeśli encja jest odwołaniem do funkcji, lub typem odpowiadającego obiektu przechwyconego w innym przypadku.

Te [m]środki, które zmienna mw fchwyta kopii. Obiekt mjest odniesieniem do obiektu, więc typ zamknięcia ma element członkowski, którego typ jest typem odwołania. Oznacza to, że typ członka jest int, a nie int&.

Ponieważ nazwa mwewnątrz ciała lambda nazywa element członkowski obiektu zamknięcia, a nie zmienną w f(i jest to wątpliwa część), instrukcja m += 123;modyfikuje ten element, który jest innym intobiektem niż ::n.

aschepler
źródło