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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/12202656/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-27 16:01:01  来源:igfitidea点击:

C++11 lambda implementation and memory model

c++memorylambdac++11

提问by Steve

I would like some information on how to correctly think about C++11 closures and std::functionin terms of how they are implemented and how memory is handled.

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

Although I don't believe in premature optimisation, I do have a habit of carefully considering the performance impact of my choices while writing new code. I also do a fair amount of real-time programming, e.g. on microcontrollers and for audio systems, where non-deterministic memory allocation/deallocation pauses are to be avoided.

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

Therefore I'd like to develop a better understanding of when to use or not use C++ lambdas.

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

My current understanding is that a lambda with no captured closure is exactly like a C callback. However, when the environment is captured either by value or by reference, an anonymous object is created on the stack. When a value-closure must be returned from a function, one wraps it in std::function. What happens to the closure memory in this case? Is it copied from the stack to the heap? Is it freed whenever the std::functionis freed, i.e., is it reference-counted like a std::shared_ptr?

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

I imagine that in a real-time system I could set up a chain of lambda functions, passing B as a continuation argument to A, so that a processing pipeline A->Bis created. In this case, the A and B closures would be allocated once. Although I'm not sure whether these would be allocated on the stack or the heap. However in general this seems safe to use in a real-time system. On the other hand if B constructs some lambda function C, which it returns, then the memory for C would be allocated and deallocated repeatedly, which would not be acceptable for real-time usage.

我想象在实时系统中,我可以设置一系列 lambda 函数,将 B 作为继续参数传递给 A,从而A->B创建处理管道。在这种情况下,A 和 B 闭包将被分配一次。虽然我不确定这些是分配在堆栈上还是堆上。但是总的来说,这在实时系统中使用似乎是安全的。另一方面,如果 B 构造了一些 lambda 函数 C 并返回它,那么 C 的内存将被重复分配和释放,这对于实时使用来说是不可接受的。

In pseudo-code, a DSP loop, which I think is going to be real-time safe. I want to perform processing block A and then B, where A calls its argument. Both these functions return std::functionobjects, so fwill be a std::functionobject, where its environment is stored on the heap:

在伪代码中,一个 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)
}

And one which I think might be bad to use in real-time code:

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

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

And one where I think stack memory is likely used for the closure:

还有一个我认为堆栈内存可能用于闭包的地方:

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

In the latter case the closure is constructed at each iteration of the loop, but unlike the previous example it is cheap because it is just like a function call, no heap allocations are made. Moreover, I wonder if a compiler could "lift" the closure and make inlining optimisations.

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

Is this correct? Thank you.

这样对吗?谢谢你。

回答by Nicol Bolas

My current understanding is that a lambda with no captured closure is exactly like a C callback. However, when the environment is captured either by value or by reference, an anonymous object is created on the stack.

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

No; it is alwaysa C++ object with an unknown type, created on the stack. A capture-less lambda can be convertedinto a function pointer (though whether it is suitable for C calling conventions is implementation dependent), but that doesn't mean it isa function pointer.

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

When a value-closure must be returned from a function, one wraps it in std::function. What happens to the closure memory in this case?

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

A lambda isn't anything special in C++11. It's an object like any other object. A lambda expression results in a temporary, which can be used to initialize a variable on the stack:

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

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

lambis a stack object. It has a constructor and destructor. And it will follow all of the C++ rules for that. The type of lambwill contain the values/references that are captured; they will be members of that object, just like any other object members of any other type.

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

You can give it to a std::function:

你可以把它给一个std::function

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

In this case, it will get a copyof the value of lamb. If lambhad captured anything by value, there would be two copies of those values; one in lamb, and one in func_lamb.

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

When the current scope ends, func_lambwill be destroyed, followed by lamb, as per the rules of cleaning up stack variables.

当前作用域结束时,func_lamb将被销毁,然后是lamb,按照清理堆栈变量的规则。

You could just as easily allocate one on the heap:

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

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

Exactly where the memory for the contents of a std::functiongoes is implementation-dependent, but the type-erasure employed by std::functiongenerally requires at least one memory allocation. This is why std::function's constructor can take an allocator.

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

Is it freed whenever the std::function is freed, i.e., is it reference-counted like a std::shared_ptr?

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

std::functionstores a copyof its contents. Like virtually every standard library C++ type, functionuses value semantics. Thus, it is copyable; when it is copied, the new functionobject is completely separate. It is also moveable, so any internal allocations can be transferred appropriately without needing more allocating and copying.

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

Thus there is no need for reference counting.

因此不需要引用计数。

Everything else you state is correct, assuming that "memory allocation" equates to "bad to use in real-time code".

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

回答by barney

C++ lambda is just a syntactic sugar around (anonymous) Functor class with overloaded operator()and std::functionis just a wrapper around callables (i.e functors, lambdas, c-functions, ...) which does copy by valuethe "solid lambda object" from the current stack scope - to the heap.

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

To test the number of actual constructors/relocatons I made a test (using another level of wrapping to shared_ptr but its not the case). See for yourself:

为了测试实际构造函数/重定位的数量,我做了一个测试(使用另一个级别的包装到 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)();
}

it makes this output:

它使这个输出:

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

Exactly same set of ctors/dtors would be called for the stack-allocated lambda object! (Now it calls Ctor for stack allocation, Copy-ctor (+ heap alloc) to construct it in std::function and another one for making shared_ptr heap allocation + construction of function)

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