В C ++ 11, когда предполагаемые переменные лямбда-выражения должны быть захвачены по значению? - PullRequest
8 голосов
/ 24 октября 2011

У меня есть программа Visual Studio 2010 C ++, основная функция которой:

vector<double> v(10);

double start = 0.0; double increment = 10.0;
auto f = [&start, increment]() { return start += increment; };
generate(v.begin(), v.end(), f);
for(auto it = v.cbegin(); it != v.cend(); ++it) { cout << *it << ", "; }

cout << endl << "Changing vars to try again..." << endl;
start = 15; increment = -1.5;
generate(v.begin(), v.end(), f);
for(auto it = v.cbegin(); it != v.cend(); ++it) { cout << *it << ", "; }
return 0;

Когда я компилирую это в MS Visual Studio, первое генерирование делает то, что я ожидал, что приводит к "10, 20, ... 100,". Второй нет; лямбда "видит" изменение start, но не изменение increment, поэтому я получаю "25, 35, ... 115,".

MSDN объясняет , что

Компилятор Visual C ++ связывает лямбда-выражение с захваченными переменными при объявлении выражения, а не при вызове выражения. ... [T] переназначение [переменной, захваченной значением] позже в программе, не влияет на результат выражения.

Итак, мой вопрос: соответствует ли это стандартному поведению C ++ 11 или это собственная эксцентричная реализация Microsoft? Бонус: если является стандартным поведением, почему стандарт был написан таким образом? Связано ли это с обеспечением ссылочной прозрачности для функционального программирования?

Ответы [ 4 ]

15 голосов
/ 24 октября 2011

С лямбда-выражением связанные переменные захватываются во время объявления .

Этот пример прояснит это: https://ideone.com/Ly38P

 std::function<int()> dowork()
 {
      int answer = 42;
      auto lambda = [answer] () { return answer; };

      // can do what we want
      answer = 666;
      return lambda;
 }

 int main()
 {
      auto ll = dowork();
      return ll(); // 42
 }

Ясно, что перехват должен происходить до вызова, поскольку перехваченные переменные даже не существуют (ни в области видимости, ни в момент жизни) больше в более позднее время.

6 голосов
/ 24 октября 2011

Это связано во время создания. Рассмотрим:

#include <functional>
#include <iostream>

std::function<int(int)> foo;

void sub()
{
    int a = 42;
    foo = [a](int x) -> int { return x + a; };
}

int main()
{
    sub();
    int abc = 54;
    abc = foo(abc); // Note a no longer exists here... but it was captured by
                    // value, so the caller shouldn't have to care here...
    std::cout << abc; //96
}

Здесь нет a при вызове функции - у компилятора не будет возможности вернуться и обновить его. Если вы передадите a по ссылке, то у вас будет неопределенное поведение. Но если вы передадите значение, любой разумный программист будет ожидать, что это сработает.

3 голосов
/ 25 октября 2011

Я думаю, вы путаете механизм захвата с механизмом передачи переменных. Они не одно и то же, даже если они имеют внешнее сходство друг с другом. Если вам нужно текущее значение переменной внутри лямбда-выражения, перехватите его по ссылке (хотя, конечно, эта ссылка привязана к определенной переменной в точке, где объявлена ​​лямбда).

Когда вы «захватываете» переменную, вы создаете нечто очень похожее на замыкание. И замыкания всегда статически ограничены (т. Е. «Захват» происходит в точке объявления). Люди, знакомые с концепцией лямбда-выражения, сочтут лямбда-выражения C ++ весьма странными и сбивающими с толку, если бы это было иначе. Добавление совершенно новой функции к языку программирования, которая отличается от той же функции в других языках программирования каким-либо существенным образом, сделает C ++ еще более запутанным и трудным для понимания, чем он есть. Кроме того, все остальное в C ++ имеет статическую область видимости, поэтому добавление некоторого элемента динамической области видимости также будет очень странным по этой причине.

Наконец, если захват всегда происходит по ссылке, то это будет означать, что лямбда будет действительна только до тех пор, пока действителен кадр стека. Либо вам придется добавить в C ++ кадры стека для сбора мусора (с огромным ударом по производительности и большим криком от людей, которые в основном зависят от стека), либо вы в конечном итоге создадите еще одну функцию, в которой было бы несложно легко взорвать ваш Случайно начните с базуки, так как кадр стека, на который ссылается лямбда-выражение, выйдет из области видимости, и вы в основном создадите множество невидимых возможностей для возврата локальных переменных по ссылке.

0 голосов
/ 24 октября 2011

Да, имеет для захвата по значению в точке, потому что в противном случае вы могли бы попытаться захватить переменную (например, по ссылке), которая больше не существует, когда на самом деле вызывается лямбда / функция.

Стандарт поддерживает захват как по значению, так и по ссылке, чтобы рассмотреть оба возможных варианта использования.Если вы скажете компилятору захватывать по значению, он будет захвачен в момент создания лямбды.Если вы попросите захватить по ссылке, он будет захватывать ссылку на переменную, которая затем будет использоваться в точке, где вызывается лямбда (разумеется, требуя, чтобы указанная переменная все еще существовала в точке вызовасделано).

...