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


92

我想要一些有关如何正确考虑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; }
}

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

这样对吗?谢谢。


4
使用lambda表达式时没有开销。另一种选择是自己编写一个这样的函数对象,这将完全相同。顺便说一句,关于内联问题,由于编译器具有所需的所有信息,因此可以肯定的是,可以内联对的调用operator()。没有要做的“提升”,lambda并没有什么特别的。它们只是局部函数对象的简写。
Xeo 2012年

这似乎是一个关于是否std::function将其状态存储在堆上的问题,并且与lambdas无关。那正确吗?
Mooing Duck 2012年

8
只是拼出来的任何误解的情况下:lambda表达式是不是一个std::function
Xeo 2012年

1
只是一个侧面的注释:从函数返回lambda时要小心,因为在离开创建lambda的函数之后,引用捕获的任何局部变量都将变为无效。
乔治

2
@Steve自C ++ 14起,您可以从具有auto返回类型的函数中返回lambda 。
Oktalist,

Answers:


100

我目前的理解是没有捕获的闭包的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对象是完全独立的。它也是可移动的,因此任何内部分配都可以适当地传输,而无需进行更多分配和复制。

因此,不需要参考计数。

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


1
很好的解释,谢谢。因此,创建std::function是分配和复制内存的地方。似乎可以理解,如果没有先将其复制到std::function,就没有办法返回闭包(因为它们是在堆栈上分配的)。
2012年

3
@Steve:是的;您必须将lambda包裹在某种容器中才能退出范围。
尼科尔·波拉斯

是复制整个函数的代码,还是原始函数在编译时分配并传递了封闭值?
拉马吉登(Llamageddon)

我想补充一下,标准或多或少地间接要求(第20.8.11.2.1节[func.wrap.func.con]¶5),即如果lambda不捕获任何内容,则可以将其存储在std::function没有动态内存的对象中分配正在进行。
5gon12eder

2
@Yakk:您如何定义“大”?具有两个指针状态的对象是否为“大”?3或4呢?此外,对象大小不是唯一的问题;如果对象不是不可移动对象,则必须将其存储在分配中,因为function它具有noexcept move构造函数。说“通常需要”的全部要点是,我并不是说“ 总是需要”:在某些情况下将不执行分配。
Nicol Bolas

0

C ++ lambda只是围绕(匿名)具有重载的Functor类的语法糖,operator()并且std::function只是围绕可调用项(即函子,lambda,c函数等)的包装,该可调用项确实从当前复制 “固态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对象将调用完全相同的一组ctor / dtor!(现在,它调用Ctor进行堆栈分配,使用Copy-ctor(+堆分配)在std :: function中构造它,并调用另一个用于进行shared_ptr堆分配+函数的构造)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.