Подтвердить что ты не робот

С++ 11 реализация и модель памяти

Мне нужна некоторая информация о том, как правильно думать о закрытиях С++ 11 и std::function в том, как они реализованы и как обрабатывается память.

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

Поэтому я хотел бы лучше понять, когда использовать или не использовать С++ lambdas.

Мое настоящее понимание заключается в том, что лямбда без захваченного закрытия точно напоминает обратный вызов C. Однако, когда среда захватывается либо по значению, либо по ссылке, анонимный объект создается в стеке. Когда значение-замыкание должно быть возвращено из функции, одно обертывает его в std::function. Что происходит с закрывающей памятью в этом случае? Скопирован ли он из стека в кучу? Освобождается ли он всякий раз, когда освобождается std::function, т.е. Считается ли оно отсчетным как a std::shared_ptr?

Я предполагаю, что в системе реального времени я мог бы создать цепочку лямбда-функций, передав B в качестве аргумента продолжения A, чтобы был создан конвейер обработки A->B. В этом случае замыкания А и В будут выделены один раз. Хотя я не уверен, будут ли они выделены в стеке или куче. Однако в целом это кажется безопасным для использования в системе реального времени. С другой стороны, если B создает некоторую лямбда-функцию C, которую он возвращает, то память для C будет распределяться и освобождаться повторно, что неприемлемо для использования в режиме реального времени.

В псевдокоде, цикле DSP, который, я думаю, будет безопасным в режиме реального времени. Я хочу выполнить блок обработки A, а затем B, где A вызывает его аргумент. Обе эти функции возвращают объекты std::function, поэтому f будет объектом std::function, где его среда хранится в куче:

auto f = A(B);  // A returns a function which calls B
                // Memory for the function returned by A is on the heap?
                // Note that A and B may maintain a state
                // via mutable value-closure!
for (t=0; t<1000; t++) {
    y = f(t)
}

И тот, который, я думаю, может быть плохим для использования в режиме реального времени:

for (t=0; t<1000; t++) {
    y = A(B)(t);
}

И тот, где, я думаю, стек стека, вероятно, используется для закрытия:

freq = 220;
A = 2;
for (t=0; t<1000; t++) {
    y = [=](int t){ return sin(t*freq)*A; }
}

В последнем случае замыкание строится на каждой итерации цикла, но, в отличие от предыдущего примера, оно дешево, потому что оно подобно вызову функции, никакие распределения кучи не производятся. Более того, мне интересно, может ли компилятор "поднять" закрытие и сделать оптимизацию вложения.

Это правильно? Спасибо.

4b9b3361

Ответ 1

Мое настоящее понимание заключается в том, что лямбда без захваченного закрытия точно напоминает обратный вызов C. Однако, когда среда захватывается либо по значению, либо по ссылке, анонимный объект создается в стеке.

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

Когда значение-замыкание должно быть возвращено из функции, одно обертывает его в std:: function. Что происходит с памятью замыкания в этом случае?

Лямбда не является чем-то особенным в С++ 11. Это объект, как любой другой объект. Выражение лямбда приводит к временному, которое может быть использовано для инициализации переменной в стеке:

auto lamb = []() {return 5;};

lamb - объект стека. Он имеет конструктор и деструктор. И для этого он будет следовать всем правилам С++. Тип lamb будет содержать значения/ссылки, которые будут захвачены; они будут членами этого объекта, как и любые другие объекты любого другого типа.

Вы можете передать его std::function:

auto func_lamb = std::function<int()>(lamb);

В этом случае он получит копию значения lamb. Если lamb захватил что-либо по значению, было бы две копии этих значений; один в lamb и один в func_lamb.

Когда текущая область действия заканчивается, func_lamb будет уничтожена, а затем lamb, в соответствии с правилами очистки переменных стека.

Вы можете так же легко выделить один в куче:

auto func_lamb_ptr = new std::function<int()>(lamb);

Именно там, где память для содержимого std::function идет, зависит от реализации, но стирание типа, используемое std::function, обычно требует по меньшей мере одного распределения памяти. Вот почему конструктор std::function может принимать распределитель.

Он освобождается всякий раз, когда функция std:: освобождается, т.е. ссылается ли она на std:: shared_ptr?

std::function хранится копия его содержимого. Как и практически каждый стандартный тип библиотеки С++, function использует семантику значений. Таким образом, он можно копировать; при копировании новый объект function полностью разделен. Он также может перемещаться, поэтому любые внутренние распределения могут быть переданы соответствующим образом, не требуя большего выделения и копирования.

Таким образом, нет необходимости в подсчете ссылок.

Все остальное, что вы заявляете, правильное, если предположить, что "распределение памяти" соответствует "плохому использованию в реальном времени".

Ответ 2

C++ lambda - это просто синтаксический сахар вокруг (анонимный). Класс Functor с перегруженными operator() и std::function - это просто оболочка вокруг вызываемых объектов (т.е. функторы, lambdas, c-functions,...), которые копируют по значению "сплошной лямбда-объект" из текущей области стека - в кучу.

Чтобы проверить количество фактических конструкторов/релокатонов, я сделал тест (используя другой уровень обертывания для shared_ptr, но это не так). Посмотреть на себя:

#include <memory>
#include <string>
#include <iostream>

class Functor {
    std::string greeting;
public:

    Functor(const Functor &rhs) {
        this->greeting = rhs.greeting;
        std::cout << "Copy-Ctor \n";
    }
    Functor(std::string _greeting="Hello!"): greeting { _greeting } {
        std::cout << "Ctor \n";
    }

    Functor & operator=(const Functor & rhs) {
        greeting = rhs.greeting;
        std::cout << "Copy-assigned\n";
        return *this;
    }

    virtual ~Functor() {
        std::cout << "Dtor\n";
    }

    void operator()()
    {
        std::cout << "hey" << "\n";
    }
};

auto getFpp() {
    std::shared_ptr<std::function<void()>> fp = std::make_shared<std::function<void()>>(Functor{}
    );
    (*fp)();
    return fp;
}

int main() {
    auto f = getFpp();
    (*f)();
}

он делает этот вывод:

Ctor 
Copy-Ctor 
Copy-Ctor 
Dtor
Dtor
hey
hey
Dtor

Точно такой же набор ctors/dtors вызывается для выделенного стеком лямбда-объекта! (Теперь он вызывает Ctor для распределения стека, Copy-ctor (+ heap alloc), чтобы построить его в std :: function, а другой для создания распределения кучи shared_ptr + построения функции)