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

Posted

技术标签:

【中文标题】C++11 lambda 实现和内存模型【英文标题】:C++11 lambda implementation and memory model 【发布时间】:2012-08-25 12:32:33 【问题描述】:

我想要一些关于如何正确思考 C++11 闭包和 std::function 的信息,了解它们的实现方式和内存处理方式。

虽然我不相信过早的优化,但我确实有一个习惯,即在编写新代码时仔细考虑我的选择对性能的影响。我也做了相当多的实时编程,例如在微控制器和音频系统上,要避免非确定性的内存分配/释放暂停。

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

我目前的理解是,没有捕获闭包的 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)

我认为在实时代码中使用它可能不好:

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; 

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

这是正确的吗?谢谢。

【问题讨论】:

使用 lambda 表达式时没有开销。另一种选择是自己编写这样一个函数对象,这将完全相同。顺便说一句,在内联问题上,由于编译器拥有它需要的所有信息,它肯定可以内联对operator() 的调用。没有“提升”可做,lambdas 没有什么特别的。它们只是局部函数对象的简写。 这似乎是一个关于std::function是否将其状态存储在堆上的问题,与lambdas无关。对吗? 只是为了防止误解:lambda表达式不是 std::function!! 附注:从函数返回 lambda 时要小心,因为通过引用捕获的任何局部变量在离开创建 lambda 的函数后都会变得无效。 @Steve 从 C++14 开始,您可以从具有 auto 返回类型的函数返回 lambda。 【参考方案1】:

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

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

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

lambda 在 C++11 中并没有什么特别之处。它是一个像任何其他对象一样的对象。一个 lambda 表达式会产生一个临时的,可用于初始化堆栈上的变量:

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

std::function 内容的确切内存位置取决于实现,但std::function 使用的类型擦除通常需要至少一个内存分配。这就是std::function 的构造函数可以采用分配器的原因。

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

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

因此不需要引用计数。

假设“内存分配”等同于“在实时代码中使用不好”,您所说的所有其他内容都是正确的。

【讨论】:

很好的解释,谢谢。所以std::function的创建是内存被分配和复制的地方。似乎没有办法返回一个闭包(因为它们是在堆栈上分配的),没有先复制到std::function,是吗? @Steve:是的;您必须将 lambda 包装在某种容器中才能退出范围。 是复制了整个函数的代码,还是原函数编译时分配并传递了封闭值? 我想补充一点,标准或多或少间接地规定(§ 20.8.11.2.1 [func.wrap.func.con] ¶ 5)如果 lambda 没有捕获任何东西,它可以存储在std::function 对象中,无需进行动态内存分配。 @Yakk:你如何定义“大”?具有两个状态指针的对象是否“大”? 3个或4个怎么样?此外,对象大小不是唯一的问题。如果对象不是不可移动的,它必须存储在分配中,因为function 有一个 noexcept 移动构造函数。说“一般要求”的全部意义在于我不是说“总是要求”:在某些情况下不会执行分配。【参考方案2】:

C++ lambda 只是(匿名)Functor 类的语法糖,重载 operator()std::function 只是可调用对象(即 functors、lambdas、c-functions ......)的包装器,它确实 按值将“solid lambda 对象”从当前堆栈范围复制到

为了测试实际构造函数/重定位的数量,我做了一个测试(对 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

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

【讨论】:

以上是关于C++11 lambda 实现和内存模型的主要内容,如果未能解决你的问题,请参考以下文章

c++11 内存模型解读

Java内存模型

Java内存模型

java的内存模型

Java虚拟机:十Java内存模型

深入理解 Java 内存模型