Різниця в поведінці змінної функції лямбда від посилання на глобальну змінну


22

Я виявив, що результати компіляторів відрізняються, якщо я використовую лямбда, щоб захопити посилання на глобальну змінну із змінним ключовим словом, а потім змінити значення у функції лямбда.

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

Результат від VS 2015 та GCC (g ++ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.12) 5.4.0 20160609):

100 223 100

Результат від clang ++ (clang версія 3.8.0-2ubuntu4 (теги / RELEASE_380 / final)):

100 223 223

Чому це відбувається? Чи це дозволено стандартами C ++?


Поведінка Кланг все ще присутня на багажнику.
волоський горіх

Це все досить старі версії компілятора
MM

Він все ще представлений в останній версії Clang: godbolt.org/z/P9na9c
Віллі,

1
Якщо ви видалите захоплення повністю, GCC все одно приймає цей код і робить те, що робить кланг. Це сильний натяк на те, що є помилка GCC - прості захоплення не повинні змінювати значення тіла лямбда.
ТЦ

Відповіді:


16

Лямбда не може захопити посилання сам за значенням (використання std::reference_wrapperдля цієї мети).

У вашій лямбда [m]фіксує mзначення за значенням (тому що його немає &у захопленні), тому m(будучи посиланням на нього n) спочатку відміняється, а копія речі, на яку він посилається ( n), захоплюється. Це не відрізняється від цього:

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

Потім лямбда змінює цю копію, а не оригінал. Це те, що ви бачите, відбувається на виходах VS та GCC, як очікувалося.

Вихід Clang невірний, і його слід повідомляти як помилку, якщо він ще не був.

Якщо ви хочете , щоб ваш лямбда змінювати n, уловлювання mпо посиланню , а: [&m]. Це не відрізняється від призначення одного посилання іншому, наприклад:

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

Або, ви можете просто позбутися mповністю і захоплення nза посиланням , а: [&n].

Хоча, оскільки nзнаходиться в глобальному масштабі, його насправді зовсім не потрібно захоплювати, лямбда може отримати доступ до нього в усьому світі, не захоплюючи його:

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

5

Я думаю, що Кланг насправді може бути правильним.

Згідно з [lambda.capture] / 11 , ідентифікаційний вираз, який використовується в лямбда, відноситься до захопленого під копію лямбда члена, лише якщо він представляє собою стійке використання . Якщо цього немає, то він посилається на оригінальну сутність . Це стосується всіх версій C ++, починаючи з C ++ 11.

Відповідно до [basic.dev.odr] / 3 C ++ 17, опорна змінна не застосовується, якщо застосувати до неї перетворення lvalue в rvalue дає постійний вираз.

Однак у проекті C ++ 20 вимога щодо перетворення значення-в -значення відмінена, і відповідний пасаж змінюється багаторазово, щоб включати або не включати перетворення. Див. Випуск CWG 1472 та випуск CWG 1741 , а також відкритий випуск CWG 2083 .

Оскільки mініціалізується постійним виразом (посилається на статичний об'єкт тривалості зберігання), його використання дає постійний вираз за виключенням у [expr.const] /2.11.1 .

Однак це не так, якщо застосовуються конверсії lvalue-to-rvalue, оскільки значення nне використовується в постійному виразі.

Тому, залежно від того, чи слід застосовувати конверсії lvalue-to-rvalue при визначенні odr-використання, коли ви використовуєте mв лямбда, він може чи не може стосуватися члена лямбда.

Якщо перетворення слід застосувати, GCC та MSVC є правильними, інакше Clang є.

Ви можете бачити, що Кланг змінює свою поведінку, якщо ви змінюєте ініціалізацію, mщоб більше не бути постійним виразом:

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

У цьому випадку всі компілятори згодні з тим, що результат є

100 223 100

тому що mв лямбда буде посилатися на член закриття, який має тип intкопіювання, ініціалізований із змінної змінної mв f.


Чи правильні обидва результати VS / GCC & Clang? Або лише один із них?
Віллі

[basic.dev.odr] / 3 говорить, що змінна mзастосовується виразом, який називає її, якщо тільки застосування перетворення lvalue до rvalue не буде постійним виразом. Згідно [expr.const] / (2.7), це перетворення не було б основним постійним виразом.
aschepler

Якщо результат Кланг правильний, я вважаю, що це якось протикоректно. Оскільки з точки зору програміста, йому потрібно переконатися, що змінна, яку він записує у списку захоплення, фактично копіюється для змінного випадку, і ініціалізація m може бути змінена програмістом пізніше чомусь.
Віллі

1
m += 123;Ось mур-вживаний.
Олів

1
Я думаю, що Кланг має рацію в нинішньому формулюванні, і хоча я ще не вникав у це, відповідні зміни тут майже напевно є всіми ЗР.
ТЦ

4

Це не дозволено стандартом C ++ 17, але це може бути деякими іншими проектами стандарту. Це складно, з причин, не пояснених у цій відповіді.

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

Для кожного об'єкта, захопленого копією, неназваний нестатичний член даних оголошується у типі закриття. Порядок декларування цих членів не визначений. Тип такого члена даних є посилальним типом, якщо суб'єктом є посилання на об'єкт, значення посилання на значення типу посилається, якщо сутність є посиланням на функцію, або тип відповідного захопленого об'єкта в іншому випадку.

Це [m]означає, що змінна mв fвловлюється копією. Суб'єкт mє посиланням на об'єкт, тому тип закриття має член, тип якого є посилальним типом. Тобто тип члена є int, а ні int&.

Оскільки ім'я mвсередині тіла лямбда називає член об'єкта закриття, а не змінну в f(і це сумнівна частина), оператор m += 123;змінює цей член, який є іншим intоб'єктом ::n.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.