Реализация лямбда-выражения C ++ 11 и модель памяти

97

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

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

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

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

Я представляю себе, что в системе реального времени я мог бы настроить цепочку лямбда-функций, передав B в качестве аргумента продолжения A, чтобы был создан конвейер обработки A->B. В этом случае замыкания 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; }
}

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

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

Стив
источник
4
Использование лямбда-выражения не требует дополнительных затрат. Другой вариант - написать такой функциональный объект самостоятельно, что будет точно так же. Кстати, по встроенному вопросу, поскольку у компилятора есть вся необходимая информация, он наверняка может просто встроить вызов в operator(). Никакого "подъема" делать не надо, лямбды - это не что иное, как. Это просто сокращение от локального функционального объекта.
Xeo
Кажется, это вопрос о том, std::functionхранит ли свое состояние в куче или нет, и не имеет ничего общего с лямбдами. Это правильно?
Mooing Duck
8
Просто записать это в случае каких - либо недоразумений: выражение Лямбда это неstd::function !!
Xeo
1
Просто побочный комментарий: будьте осторожны при возврате лямбды из функции, поскольку любые локальные переменные, захваченные по ссылке, становятся недействительными после выхода из функции, создавшей лямбда.
Джорджио
2
@Steve, начиная с C ++ 14, вы можете возвращать лямбда из функции с autoвозвращаемым типом.
Oktalist 03

Ответы:

104

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

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

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

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

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

lambявляется объектом стека. У него есть конструктор и деструктор. И для этого он будет следовать всем правилам C ++. Тип 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);

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

Освобождается ли он всякий раз, когда освобождается std :: function, т. Е. Подсчитывается ли он как std :: shared_ptr?

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

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

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

Никол Болас
источник
1
Отличное объяснение, спасибо. Таким образом, создание std::function- это точка, в которой память выделяется и копируется. Из этого следует, что нет способа вернуть закрытие (поскольку они размещены в стеке) без предварительного копирования в std::function, да?
Стив
3
@ Стив: Да; вам нужно обернуть лямбду в какой-то контейнер, чтобы она вышла из области видимости.
Никол Болас
Копируется ли весь код функции, или исходная функция выделяет время компиляции и передает закрытые значения?
Llamageddon
Я хочу добавить, что стандарт более или менее косвенно предписывает (§ 20.8.11.2.1 [func.wrap.func.con] ¶ 5), что если лямбда ничего не захватывает, ее можно сохранить в std::functionобъекте без динамической памяти. выделение происходит.
5gon12eder
2
@Yakk: Как вы определяете «большой»? Является ли объект с двумя указателями состояния "большим"? Как насчет 3 или 4? Кроме того, размер объекта - не единственная проблема; Если объект не может быть перемещен без возможности перемещения, он должен быть сохранен в выделении, поскольку functionимеет конструктор перемещения noexcept. Смысл выражения «обычно требуется» состоит в том, что я не говорю « всегда требует»: существуют обстоятельства, при которых не будет выполняться выделение.
Никол Болас,
1

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

Чтобы проверить количество реальных конструкторов / перемещений, я провел тест (используя другой уровень переноса на 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 + построение функции)

Барни
источник