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

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

#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 и G CC (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 ++?

Ответы [ 3 ]

16 голосов
/ 27 марта 2020

Лямбда не может захватить ссылку сама по значению (используйте std::reference_wrapper для этой цели).

В вашей лямбде [m] захватывает m по значению ( потому что в захвате нет &), поэтому сначала разыменовывается m (являющийся ссылкой на n) и захватывается копия объекта, на который он ссылается (n) , Это ничем не отличается от этого:

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

Лямбда изменяет эту копию, а не оригинал. Это то, что вы видите на выходах VS и G CC, как и ожидалось.

Вывод Clang неверен, и его следует сообщить об ошибке, если он еще не произошел.

Если вы хотите, чтобы ваша лямбда модифицировала n, запишите m по ссылке: [&m]. Это ничем не отличается от присвоения одной ссылки другой, например:

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

Или вы можете просто полностью избавиться от m и захватить n по ссылке вместо этого: [&n].

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

return [] () -> int {
    n += 123;
    return n;
};
5 голосов
/ 27 марта 2020

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

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

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

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

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

Это не так однако, если применяются преобразования lvalue-в-значение, потому что значение n не может использоваться в константном выражении.

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

Если должно применяться преобразование, G CC и MSV C верны, в противном случае 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 в лямбде будет ссылаться на элемент замыкания, который имеет тип int, инициализированный копией из ссылочной переменной m в f.

4 голосов
/ 27 марта 2020

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

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

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

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

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

...