Я нахожу использование замыкания C++0x озадачивающим. Мой первоначальный отчет и последующий вызвали больше путаницы, чем объяснения. Ниже я покажу вам неприятные примеры и надеюсь выяснить, почему в коде возникает неопределенное поведение. Все фрагменты кода проходят компилятор gcc 4.6.0 без каких-либо предупреждений.
Как мое использование std::function нарушает код? почему программы №1-3 работают хорошо, а программа №4 корректна при трехкратном(!) вызове ac(1)? Почему программа № 4 застревает в следующих трех случаях, как если бы переменная x была захвачена по значению, а не по ссылке. И последние три вызова ac(1) совершенно непредсказуемы, как если бы любая ссылка на x была потеряна.
Я надеюсь выяснить, почему в коде есть неопределенное поведение
Каждый раз, когда я имею дело со сложными и запутанными лямбда-выражениями, мне кажется, что проще сначала выполнить перевод в форму объекта-функции. Потому что лямбды - это просто синтаксический сахар для объекта-функции, и для каждой лямбды существует однозначное сопоставление с соответствующим объектом-функцией. В этой статье очень хорошо объясняется, как сделать перевод: http://blogs.msdn.com/b/vcblog/archive/2008/10/28/lambdas-auto-and-static-assert-c-0x-features-in-vc10-part-1.aspx
Здесь конструктор InnerAccumulator возьмет ссылку на x, локальную переменную, которая исчезнет, как только вы выйдете из области действия operator(). Так что да, вы просто получаете простое старое доброе неопределенное поведение, как вы и подозревали.
Ответ 2 · рейтинг 2 · JohannesD
Давайте попробуем что-нибудь совершенно невинное:
#include <iostream>
int main(){
auto accumulator = [](int x) {
return [&](int y) -> int {
return x+=y;
};
};
auto ac=accumulator(1);
//// Surely this should be a no-op?
accumulator(666);
//// There are no side effects and we throw the result away!
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
}
Тада:
669 668 667
672 671 670
675 674 673
Конечно, это тоже не гарантированное поведение. Действительно, с включенной оптимизацией gcc устранит вызов accumulator(666), определяющий его мертвый код, и мы снова получим исходные результаты. И это полностью в его праве сделать это; в соответствующей программе удаление вызова действительно не повлияет на семантику. Но в области неопределенного поведения может произойти все.
Опять же, C++ не обеспечивает и не может обеспечить истинные лексические замыкания, в которых время жизни локальных переменных выходит за пределы их исходной области видимости. Это повлечет за собой внедрение в язык сборки мусора и локальных переменных из кучи.
Однако все это довольно академично, поскольку захват x путем копирования делает программу четко определенной и работает так, как ожидалось:
auto accumulator = [](int x) {
return [x](int y) mutable -> int {
return x += y;
};
};
Ответ 3 · рейтинг 1 · MSalters
Ну, ссылки становятся висящими, когда референт уходит. У вас очень хрупкий дизайн, если объект A имеет ссылку на какую-то часть объекта B, если только объект A каким-то образом не может гарантировать время жизни объекта B (например, когда A все равно содержит shared_ptr для B, или оба находятся в тот же объем).
Ссылки в лямбда-выражениях не являются волшебным исключением. Если вы планируете вернуть ссылку на x+=y, вам лучше убедиться, что x живет достаточно долго. Здесь аргумент int x инициализируется как часть вызова accumulator(1). Время жизни аргумента функции заканчивается, когда функция возвращается.