Неопределенное поведение с замыканием C++0x: II

Я нахожу использование замыкания C++0x озадачивающим. Мой первоначальный отчет < /a> и последующий вызвали больше путаницы, чем объяснения. Ниже я покажу вам неприятные примеры и надеюсь выяснить, почему в коде возникает неопределенное поведение. Все фрагменты кода проходят компилятор gcc 4.6.0 без каких-либо предупреждений.

Программа №1: Это работает

#include <iostream>
int main(){
    auto accumulator = [](int x) {
        return [=](int y) -> int { 
            return x+y;
        }; 
    };
    auto ac=accumulator(1);
    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;
}

Результат соответствует ожиданиям:

2 2 2

2 2 2

2 2 2

2. Программа №2: Замыкание, работает нормально

#include <iostream>
int main(){
    auto accumulator = [](int x) {
        return [&](int y) -> int { 
            return x+=y;
        }; 
    };
    auto ac=accumulator(1);
    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;
}

Результат:

4 3 2

7 6 5

10 9 8

Программа 3: Программа № 1 с std::function, работает нормально

#include <iostream>
#include <functional>     // std::function

int main(){

    typedef std::function<int(int)> fint2int_type;
    typedef std::function<fint2int_type(int)> parent_lambda_type;

    parent_lambda_type accumulator = [](int x) -> fint2int_type{
        return [=](int y) -> int { 
            return x+y;
        }; 
    };

    fint2int_type ac=accumulator(1);

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

Результат:

2 2 2

2 2 2

2 2 2

Программа 4: Программа № 2 с std::function, неопределенное поведение

#include <iostream>
#include <functional>     // std::function

int main(){

    typedef std::function<int(int)> fint2int_type;
    typedef std::function<fint2int_type(int)> parent_lambda_type;

    parent_lambda_type accumulator = [](int x) -> fint2int_type{
        return [&](int y) -> int { 
            return x+=y;
        }; 
    };

    fint2int_type ac=accumulator(1);

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

Первый запуск программы дает:

4 3 2

4 3 2

12364812 12364811 12364810

Второй запуск той же программы:

4 3 2

4 3 2

1666060 1666059 1666058

Третий:

4 3 2

4 3 2

2182156 2182155 2182154

Как мое использование std::function нарушает код? почему программы №1-3 работают хорошо, а программа №4 корректна при трехкратном(!) вызове ac(1)? Почему программа № 4 застревает в следующих трех случаях, как если бы переменная x была захвачена по значению, а не по ссылке. И последние три вызова ac(1) совершенно непредсказуемы, как если бы любая ссылка на x была потеряна.


person Community    schedule 06.04.2011    source источник


Ответы (3)


Я надеюсь выяснить, почему в коде есть неопределенное поведение

Каждый раз, когда я имею дело со сложными и запутанными лямбда-выражениями, мне кажется, что проще сначала выполнить перевод в форму объекта-функции. Потому что лямбды - это просто синтаксический сахар для объекта-функции, и для каждой лямбды существует однозначное сопоставление с соответствующим объектом-функцией. В этой статье очень хорошо объясняется, как сделать перевод: http://blogs.msdn.com/b/vcblog/archive/2008/10/28/lambdas-auto-and-static-assert-c-0x-features-in-vc10-part-1.aspx

Так, например, ваша программа № 2:

#include <iostream>
int main(){
    auto accumulator = [](int x) {
        return [&](int y) -> int { 
            return x+=y;
        }; 
    };
    auto ac=accumulator(1);
    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;
}

будет приблизительно переведено компилятором в это:

#include <iostream>

struct InnerAccumulator
{
    int& x;
    InnerAccumulator(int& x):x(x)
    {
    }
    int operator()(int y) const
    {
        return x+=y;
    }
};

struct Accumulator
{
    InnerAccumulator operator()(int x) const
    {
        return InnerAccumulator(x); // constructor
    }
};


int main()
{
    Accumulator accumulator;
    InnerAccumulator ac = accumulator(1);
    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;
}

И теперь проблема стала совершенно очевидной:

InnerAccumulator operator()(int x) const
{
   return InnerAccumulator(x); // constructor
}

Здесь конструктор InnerAccumulator возьмет ссылку на x, локальную переменную, которая исчезнет, ​​как только вы выйдете из области действия operator(). Так что да, вы просто получаете простое старое доброе неопределенное поведение, как вы и подозревали.

person Thomas Petit    schedule 06.04.2011
comment
Спасибо за объяснение, по крайней мере, я получил направление для работы. Проблема в том, что представленная вами демонстрация не объясняет, почему Программа 2 работает нормально. Он реализует лексическую область видимости, где x живет как состояние в пределах области действия main, как и предполагалось. Проблема где-то с использованием std::function в программе 4, я надеялся получить некоторую информацию о ее внутренностях в этом конкретном случае, но это кажется сложнее, чем кажется. Но тем не менее я ценю ваш ответ, и я буду продолжать искать объяснение. - person ; 07.04.2011
comment
предоставленная вами демонстрация не объясняет, почему программа 2 работает нормально. Это неопределенное поведение для вас. В программе № 2.2 используется оборванная ссылка. Она может абсолютно все: вылететь сразу, вылететь позже, выдать исключение, выдать случайный результат (как в Программе 4), запустить ядерные ракеты, превратив мир в радиоактивную пустошь и т. д. Хуже того, она тоже может работать! Но это просто удача, другой компилятор покажет другое поведение. Например, мой код с функцией-объектом работает с GCC 4.6 (он печатает 4 3 2 7 6 5 10 9 8), но сбой с MSVC10. (вы получаете нарушение прав доступа). - person Thomas Petit; 07.04.2011
comment
Я определяю undefined, когда результат непредсказуем. Программа № 2 проходит компилятор и выдает ожидаемый результат, поскольку x находится в области видимости main. Это выглядит необычно для человека, привыкшего к блочной области видимости, но для программиста лексической области видимости это поведение по умолчанию. Я бы не стал оспаривать ваше замечание, что это пример небезопасного программирования, но это вопрос стиля. - person ; 07.04.2011
comment
Висячая ссылка будет работать до тех пор, пока в программе ничего не испортится с областью памяти, ранее занятой значением. В случае программы 4 std::function делает некоторые вещи со стеком, которые путаются с областью, на которую указывает висячая ссылка, но это не имеет ничего общего с std::function, поскольку ответ Йоханнеса Дальстрема показывает, что многие вещи могут вызвать это случится. Самое главное: то, что он работает и делает одно и то же каждый раз, НЕ означает, что это не Undefined Behavior. - person SoapBox; 07.04.2011
comment
@Rusty: К сожалению, вы не можете определить, что означает неопределенное поведение, поскольку это технический термин, определенный в стандарте языка. Стандарт говорит, что определенные семантические конструкции вызывают неопределенное поведение, и если ваша программа включает такие конструкции, в программе есть ошибка. Это решительно ничего не имеет отношения к разным стилям программирования, это связано с тем, содержит ли ваша программа ошибки или нет! Даже если кажется, что он работает нормально с одним компилятором, с определенными параметрами компилятора, и когда звезды совпадают, ничто не гарантирует, что он будет работать нормально и завтра. - person JohannesD; 07.04.2011

Давайте попробуем что-нибудь совершенно невинное:

#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), определяющий его мертвый код, и мы снова получим исходные результаты. И это полностью в его праве сделать это; в соответствующей программе удаление вызова действительно не повлияет на семантику. Но в области неопределенного поведения может произойти все.


РЕДАКТИРОВАТЬ

auto ac=accumulator(1);

std::cout << pow(2,2) << std::endl;

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; 

Без включенных оптимизаций я получаю следующее:

4
1074790403 1074790402 1074790401 
1074790406 1074790405 1074790404 
1074790409 1074790408 1074790407 

С включенными оптимизациями,

4
4 3 2 
7 6 5 
10 9 8

Опять же, C++ не обеспечивает и не может обеспечить истинные лексические замыкания, в которых время жизни локальных переменных выходит за пределы их исходной области видимости. Это повлечет за собой внедрение в язык сборки мусора и локальных переменных из кучи.

Однако все это довольно академично, поскольку захват x путем копирования делает программу четко определенной и работает так, как ожидалось:

auto accumulator = [](int x) {
    return [x](int y) mutable -> int { 
        return x += y;
    }; 
};
person JohannesD    schedule 06.04.2011
comment
Я понимаю ваше утверждение, и все это очень ценно знать, но что именно не определено в этой программе? Вы повторно инициализируете x суммой первых семи квадратных простых чисел, и с этого момента в рамках main он накапливает единицу, как и предполагалось. Если кто-то присылает мне код, и я могу сказать, что он делает, даже не запуская его, а после запуска кода с повторениями я все равно вижу, что он делает то, что я от него ожидаю, то как поведение программы может быть неопределенным ?! Возможно, мой взгляд слишком узок, но ваш пример меня не убеждает. Программа 4 делает, хотя... - person ; 07.04.2011
comment
Вы, кажется, полностью запутались в том, как должны работать параметры функции. Конечно, ни в одном другом языке с лексическими замыканиями вы не можете повторно инициализировать захваченные переменные таким образом... JavaScript так не работает. ЛИСП так не работает. Локальные переменные отдельных вызовов одной и той же функции полностью различны и не могут каким-либо образом влиять друг на друга. - person JohannesD; 07.04.2011
comment
Я думаю, что теоретически все вызовы operator << должны иметь тот же эффект, что и функция pow() или вызов accumulator(666), искажая хранилище, содержащее x, по крайней мере, для второй и третьей строк кода. Интересно, как ему так повезло, что это не так. - person Ken Bloom; 09.04.2011

Ну, ссылки становятся висящими, когда референт уходит. У вас очень хрупкий дизайн, если объект A имеет ссылку на какую-то часть объекта B, если только объект A каким-то образом не может гарантировать время жизни объекта B (например, когда A все равно содержит shared_ptr для B, или оба находятся в тот же объем).

Ссылки в лямбда-выражениях не являются волшебным исключением. Если вы планируете вернуть ссылку на x+=y, вам лучше убедиться, что x живет достаточно долго. Здесь аргумент int x инициализируется как часть вызова accumulator(1). Время жизни аргумента функции заканчивается, когда функция возвращается.

person MSalters    schedule 06.04.2011
comment
Я рад получить вашу помощь, ваш комментарий ценен и по существу. Но что касается вашего последнего предложения, я должен сказать следующее: время жизни аргумента функции может не закончиться, когда функция вернется, это главный парадокс лексическая область. Подтверждающим доказательством этого феномена является Программа 2. Пр. 4 ведет себя именно так, как вы бы описали. Мне нравится ваш комментарий о хрупком d. Кажется, при написании замыканий на С++ нужно постоянно думать об их истинной внутренней репутации. которые нельзя игнорировать. - person ; 07.04.2011
comment
Кажется, что работает нормально никогда не является адекватным доказательством того, что что-то не является неопределенным поведением, потому что кажется, что работает нормально — это одно из возможных поведений в стране неопределенного поведения. Как я уже говорил, время жизни переменных стека не и не может быть увеличено в C++ — как это возможно? Просто попробуйте вызвать какую-нибудь другую функцию между вызовами accumulator и ac и увидите, как ваш захваченный x перезаписывается чем-то ненужным. - person JohannesD; 07.04.2011
comment
Ваше мнение ценно и глубоко ценно мной, поверьте. Но единственный практический способ увидеть, кто прав, — попробовать разные компиляторы. И беда в том, что в настоящее время мы им тоже не доверяем полностью. Но я надеюсь, что это все равно скоро решится. В противном случае нет оснований что-либо добавлять в пользу той или иной точки зрения. Настоящая проблема, которая меня беспокоит, заключается в том, что после определенного опыта я не очень хочу использовать лямбда-выражения в чем-то большем, чем обычные косметические корректировки кода STL, потому что, как вы можете видеть, в таких простых случаях уже появляются темные углы. - person ; 07.04.2011
comment
@Rusty James: время жизни аргумента функции C++ положительно заканчивается, когда функция возвращается. Цитирование статьи Javascript не имеет отношения к вашему аргументу. Большинство компиляторов C++ не пытаются принудительно применять это правило, но оно все же остается правилом. Это как превышение скорости: то, что ваша машина может разогнаться до 150, не означает, что это разрешено, и полиция тоже не всегда будет вас штрафовать. - person MSalters; 07.04.2011
comment
@Rusty James: Это не имеет ничего общего с мнениями! Это очень четко указано в языковом стандарте, и я могу процитировать соответствующую главу и стих, если вы того пожелаете. Почему так сложно понять концепцию, что стандарт очень четко определяет правила языка, а вы очень явно нарушаете одно из них? Тот факт, что вам часто удается избежать наказания за превышение скорости на автомагистрали, не меняет того факта, что превышение скорости является незаконным. - person JohannesD; 07.04.2011