Неопределенное поведение с C ++ 0x Закрытие: II - PullRequest
3 голосов
/ 06 апреля 2011

Я считаю использование замыкания 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, Undefined Behavior

#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 :: нарушает код? почему программы № 1-3 работают хорошо, а программа № 4 корректна при вызове ac (1) трижды (!)? Почему Программа № 4 застревает в следующих трех случаях, как если бы переменная x была захвачена по значению, а не по ссылке. И последние три вызова ac ​​(1) совершенно непредсказуемы, как если бы любая ссылка на x была бы потеряна.

Ответы [ 3 ]

9 голосов
/ 06 апреля 2011

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

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

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


EDIT

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;
    }; 
};
1 голос
/ 06 апреля 2011

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

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

...