Bezdepbonusu7Программирование: статьи и ответы

Вопрос

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

Автор вопроса: CommunityОтветов: 3

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

closureslambdac++11pass-by-reference

Ответы

Ответ 1 · рейтинг 9 · Thomas Petit

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

Каждый раз, когда я имею дело со сложными и запутанными лямбда-выражениями, мне кажется, что проще сначала выполнить перевод в форму объекта-функции. Потому что лямбды - это просто синтаксический сахар для объекта-функции, и для каждой лямбды существует однозначное сопоставление с соответствующим объектом-функцией. В этой статье очень хорошо объясняется, как сделать перевод: 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(). Так что да, вы просто получаете простое старое доброе неопределенное поведение, как вы и подозревали.

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


Редактировать

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

Ответ 3 · рейтинг 1 · MSalters

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

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

Перевод вопроса и ответов сообщества Stack Overflow, лицензия CC BY-SA. Оригинал на Stack Overflow.

Похожие материалы