c++ 11 lambda是如何表示和传递的

How are C++11 lambdas represented and passed?

本文关键字:表示 lambda 何表示 c++      更新时间:2023-10-16

在c++11中传递lambda非常容易:

func( []( int arg ) {
  // code
} ) ;

但我想知道,传递一个lambda给这样的函数的成本是什么?如果函数将lambda传递给其他函数呢?

void func( function< void (int arg) > f ) {
  doSomethingElse( f ) ;
}

传递lambda代价大吗?由于function对象可以赋值为0,因此

function< void (int arg) > f = 0 ; // 0 means "not init" 

让我觉得函数对象的行为有点像指针。但如果不使用new,则意味着它们可能类似于值类型的struct或类,后者默认为堆栈分配和成员复制。

如何是一个c++ 11"代码体"和组捕获的变量传递当你传递一个函数对象"按值"?是否存在大量多余的代码体副本?我是否应该用const&标记传递的每个function对象,这样就不会产生副本:

void func( const function< void (int arg) >& f ) {
}

或者函数对象以某种方式传递不同于常规c++结构?

免责声明:与现实相比,我的回答有些简化(我把一些细节放在一边),但大局在这里。此外,标准并没有完全规定lambdas或std::function必须如何在内部实现(实现有一定的自由),因此,就像任何关于实现细节的讨论一样,您的编译器可能会或可能不会完全这样做。

但是,这是一个非常类似于VTables的主题:标准没有强制要求太多,但任何明智的编译器仍然很可能这样做,所以我认为值得深入研究一下。:)


<标题>λ

实现lambda最直接的方法是一种未命名的struct:

auto lambda = [](Args...) -> Return { /*...*/ };
// roughly equivalent to:
struct {
    Return operator ()(Args...) { /*...*/ }
}
lambda; // instance of the unnamed struct

就像任何其他类一样,当您传递它的实例时,您不必复制代码,只需复制实际数据(在这里,根本没有)。


按值捕获的对象被复制到struct:

Value v;
auto lambda = [=](Args...) -> Return { /*... use v, captured by value...*/ };
// roughly equivalent to:
struct Temporary { // note: we can't make it an unnamed struct any more since we need
                   // a constructor, but that's just a syntax quirk
    const Value v; // note: capture by value is const by default unless the lambda is mutable
    Temporary(Value v_) : v(v_) {}
    Return operator ()(Args...) { /*... use v, captured by value...*/ }
}
lambda(v); // instance of the struct

再次强调,传递它只意味着传递数据(v)而不是代码本身。


同样,通过引用捕获的对象被引用到struct:

Value v;
auto lambda = [&](Args...) -> Return { /*... use v, captured by reference...*/ };
// roughly equivalent to:
struct Temporary {
    Value& v; // note: capture by reference is non-const
    Temporary(Value& v_) : v(v_) {}
    Return operator ()(Args...) { /*... use v, captured by reference...*/ }
}
lambda(v); // instance of the struct

这几乎就是关于lambda本身的所有内容(除了我提交的一些实现细节,但这些细节与理解它的工作原理无关)。


std::function

std::function是任何类型的函子(lambda,独立/静态/成员函数,像我展示的那些函子类,…)的通用包装器。

std::function的内部非常复杂,因为它们必须支持所有这些情况。根据函子的确切类型,这至少需要以下数据(给出或取实现细节):

  • 指向独立/静态函数的指针。

  • 指向函子副本的指针(动态分配,允许任何类型的函子,正如您正确注意到的)。
  • 指向要调用的成员函数的指针。
  • 指向分配器的指针,该分配器既可以复制函子,也可以复制自身(因为可以使用任何类型的函子,指向函子的指针应该是void*),因此必须有这样一种机制——可能使用多态性,即。基类+虚方法,派生类在template<class Functor> function(Functor)构造函数中本地生成)。

由于它事先不知道它要存储哪种类型的函子(std::function可以被重赋值,这一点很明显),因此它必须处理所有可能的情况并在运行时做出决定。

注意:我不知道标准要求的地方,但这肯定是一个新的副本,底层函子不共享:

int v = 0;
std::function<void()> f = [=]() mutable { std::cout << v++ << std::endl; };
std::function<void()> g = f;
f(); // 0
f(); // 1
g(); // 0
g(); // 1

因此,当您传递std::function时,它至少涉及这四个指针(实际上在GCC 4.7 64位sizeof(std::function<void()>是32,即四个64位指针)和可选的动态分配的函子副本(正如我已经说过的,仅包含捕获的对象,您不复制代码)。


对问题的回答

将lambda传递给这样的函数的代价是什么?<一口>[背景问题:价值]

嗯,正如你所看到的,它主要取决于你的函子(手工制作的struct函子或lambda)和它包含的变量。与直接按值传递struct函子相比,开销可以忽略不计,但它当然比按引用传递struct函子要高得多。

我应该用const&标记传递的每个函数对象,以便不进行复制吗?

恐怕这个问题很难用一般的方法来回答。有时你想通过const引用传递,有时通过值传递,有时通过rvalue引用传递,这样你就可以移动它。这取决于你代码的语义。 在我看来,关于你应该选择哪一个的规则是完全不同的话题,只要记住它们和任何其他对象是一样的。 无论如何,您现在拥有了做出明智决策的所有关键(同样,取决于您的代码及其语义)。

参见c++ 11 lambda实现和内存模型

lambda表达式就是一个表达式。一旦编译,它将在运行时产生一个闭包对象。

5.1.2 Lambda表达式(expr.prim.lambda)

lambda表达式的求值导致一个右值临时值(12.2)。这个临时对象称为闭包对象。

对象本身是实现定义的,在不同的编译器中可能不同。

这是lambdas在clang中的原始实现https://github.com/faisalv/clang-glambda

如果lambda可以作为一个简单的函数(即它不捕获任何东西),那么它是完全相同的方式。特别是标准要求它与具有相同签名的旧式指针函数兼容。[编辑:不准确,见评论讨论]

其余的取决于实现,但我不会提前担心。最直接的实现除了携带信息之外什么都不做。和你在抓捕时要求的一样多。所以效果和你手工创建一个类是一样的。或者使用std::bind的变体