C++ 11 lambda实现和内存模型

Ste*_*eve 88 c++ memory lambda c++11

我想了解一些关于如何正确思考C++ 11闭包以及std::function如何实现它们以及如何处理内存的信息.

虽然我不相信过早优化,但我确实习惯在编写新代码时仔细考虑我的选择对性能的影响.我还进行了大量的实时编程,例如微控制器和音频系统,其中要避免非确定性的存储器分配/解除分配暂停.

因此,我想更好地了解何时使用或不使用C++ lambdas.

我目前的理解是没有捕获闭包的lambda就像一个C回调.但是,当通过值或引用捕获环境时,将在堆栈上创建匿名对象.当必须从函数返回值闭包时,将其包装进去std::function.在这种情况下闭包内存会发生什么?它是从堆栈复制到堆?它是否在被释放时std::function被释放,即它是否被引用计数为std::shared_ptr

我想在实时系统中我可以建立一个lambda函数链,将B作为一个连续参数传递给A,以便A->B创建一个处理管道.在这种情况下,A和B闭包将被分配一次.虽然我不确定这些是否会在堆栈或堆上分配.然而,一般来说,这在实时系统中使用似乎是安全的.另一方面,如果B构造一些它返回的lambda函数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)
}
Run Code Online (Sandbox Code Playgroud)

我认为在实时代码中使用它可能很糟糕:

for (t=0; t<1000; t++) {
    y = A(B)(t);
}
Run Code Online (Sandbox Code Playgroud)

我认为堆栈内存很可能用于闭包:

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

在后一种情况下,闭包是在循环的每次迭代中构造的,但与前面的例子不同,它很便宜,因为它就像一个函数调用,没有进行堆分配.此外,我想知道编译器是否可以"解除"关闭并进行内联优化.

它是否正确?谢谢.

Nic*_*las 95

我目前的理解是没有捕获闭包的lambda就像一个C回调.但是,当通过值或引用捕获环境时,将在堆栈上创建匿名对象.

没有; 它总是一个在堆栈上创建的具有未知类型的C++对象.无捕获的lambda可以转换为函数指针(尽管它是否适合C调用约定依赖于实现),但这并不意味着它一个函数指针.

当必须从函数返回值闭包时,将其包装在std :: function中.在这种情况下闭包内存会发生什么?

lambda在C++ 11中并不特别.这是一个像任何其他对象一样的对象.lambda表达式导致临时,可用于初始化堆栈上的变量:

auto lamb = []() {return 5;};
Run Code Online (Sandbox Code Playgroud)

lamb是一个堆栈对象.它有一个构造函数和析构函数.它将遵循所有C++规则.类型lamb将包含捕获的值/引用; 它们将成为该对象的成员,就像任何其他类型的任何其他对象成员一样.

你可以把它给std::function:

auto func_lamb = std::function<int()>(lamb);
Run Code Online (Sandbox Code Playgroud)

在这种情况下,它将获得值的副本lamb.如果lamb按价值捕获了任何东西,那么这些值将有两个副本; 一个在lamb,一个在func_lamb.

当前范围结束时,func_lamb将按照lamb清理堆栈变量的规则销毁,然后销毁.

您可以轻松地在堆上分配一个:

auto func_lamb_ptr = new std::function<int()>(lamb);
Run Code Online (Sandbox Code Playgroud)

确切地说,内容的存储器std::function依赖于实现,但是所采用的类型擦除std::function通常需要至少一个存储器分配.这就是为什么std::function构造函数可以使用分配器.

每当释放std :: function时它是否被释放,即它是否像std :: shared_ptr一样被引用计数?

std::function存储其内容的副本.与几乎所有标准库C++类型一样,function使用值语义.因此,它是可复制的; 复制时,新function对象完全分开.它也是可移动的,因此任何内部分配都可以适当地传输,而无需更多的分配和复制.

因此,不需要参考计数.

假设"内存分配"等同于"在实时代码中使用不好",那么您声明的其他所有内容都是正确的.

  • @Steve:是的; 你必须将lambda包装在某种容器中,以便它退出范围. (3认同)
  • @Yakk:你如何定义"大"?是一个具有两个指针状态"大"的对象?3或4怎么样?此外,对象大小不是唯一的问题; 如果对象不是不可移动的,它必须*存储在一个分配中,因为`function`有一个noexcept移动构造函数.说"一般要求"的全部意义在于我并不是说"*总是*要求":有些情况下不会进行分配. (2认同)

bar*_*ney 5

C++ lambda 只是重载的(匿名)Functor 类的语法糖operator(),并且std::function只是可调用对象(即函子、lambda、c 函数等)的包装器,它确实从当前值复制“实体 lambda 对象”堆栈范围 - 到

为了测试实际构造函数/重定位的数量,我做了一个测试(使用另一个级别的包装到共享指针,但事实并非如此)。你自己看:

#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)();
}
Run Code Online (Sandbox Code Playgroud)

它产生这样的输出:

Ctor 
Copy-Ctor 
Copy-Ctor 
Dtor
Dtor
hey
hey
Dtor
Run Code Online (Sandbox Code Playgroud)

对于堆栈分配的 lambda 对象,将调用完全相同的 ctor/dtor 集!(现在它调用Ctor进行堆栈分配,Copy-ctor(+堆分配)在std::function中构造它,另一个用于进行shared_ptr堆分配+函数构造)