Отличие поведения изменяемого захвата лямбда-функции от ссылки на глобальную переменную

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 (tags / RELEASE_380 / final)):

100 223 223

Почему это происходит? Это разрешено стандартами C ++?

Уилли
источник
Поведение Clang все еще присутствует на стволе.
грецкий орех
Это все довольно старые версии компилятора
ММ
Это все еще присутствует на недавней версии Clang: godbolt.org/z/P9na9c
Вилли
1
Если вы полностью удалите перехват, то GCC все еще принимает этот код и делает то, что делает clang. Это сильный намек на то, что есть ошибка 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

Я думаю, что Clang действительно может быть правильным.

Согласно [lambda.capture] / 11 , id-выражение, используемое в лямбда -выражении, относится к члену lambda, захваченному при копировании, только если оно представляет собой использование odr . Если это не так, то это относится к исходной сущности . Это относится ко всем версиям C ++, начиная с C ++ 11.

Согласно [basic.dev.odr] / 3 C ++ 17 ссылочная переменная не используется odr, если применение преобразования lvalue-to-rvalue к ней дает константное выражение.

Однако в черновике C ++ 20 требование преобразования lvalue в rvalue отброшено, и соответствующий фрагмент изменялся несколько раз, чтобы включить или не включить преобразование. См РГС вопрос 1472 и РГС выпуск 1741 , а также открыт выпуск РГС 2083 .

Поскольку mинициализируется с помощью константного выражения (ссылающегося на статический объект длительности хранения), его использование приводит к константному выражению на исключение в [expr.const] /2.11.1 .

Однако это не тот случай, когда применяются преобразования lvalue в rvalue, поскольку значение nне может использоваться в константном выражении.

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

Если преобразование должно быть применено, GCC и MSVC верны, в противном случае Clang -.

Вы можете видеть, что 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в лямбде будет ссылаться на член замыкания, который имеет тип intcopy-initialized из ссылочной переменной mв f.

грецкий орех
источник
Верны ли результаты VS / GCC и Clang? Или только один из них?
Вилли
[basic.dev.odr] / 3 говорит, что переменная mиспользуется в выражении с именем odr, если ее применение к преобразованию lvalue-to-rvalue не будет константным выражением. Согласно [expr.const] / (2.7) это преобразование не будет выражением основной константы.
aschepler
Если результат Кланга правильный, я думаю, что это как-то нелогично. Потому что, с точки зрения программиста, он должен убедиться, что переменная, которую он записывает в список захвата, действительно копируется для изменяемого регистра, и инициализация m может быть изменена программистом позже по какой-то причине.
Вилли
1
m += 123;Вот mодр-б
Олив
1
Я думаю, что Clang прав в нынешней формулировке, и хотя я не углублялся в это, соответствующие изменения здесь почти наверняка являются всеми DR.
ТК
4

Это не разрешено Стандартом C ++ 17, но некоторыми другими черновиками Стандарта это может быть. Это сложно, по причинам, не объясненным в этом ответе.

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

Для каждого объекта, захваченного копией, в типе замыкания объявляется неназванный элемент не статических данных. Порядок объявления этих членов не уточняется. Тип такого члена данных является ссылочным типом, если объект является ссылкой на объект, lvalue-ссылкой на ссылочный тип функции, если объект является ссылкой на функцию, или типом соответствующего захваченного объекта в противном случае.

В [m]означает , что переменная mв fзахватывается копией. Сущность mявляется ссылкой на объект, поэтому у типа замыкания есть член, тип которого является ссылочным типом. То есть тип члена есть int, а нет int&.

Поскольку имя mвнутри лямбда-тела именует член объекта замыкания, а не переменную в f(и это сомнительная часть), оператор m += 123;изменяет этот член, который отличается intот объекта ::n.

aschepler
источник