33

在 c++11 中传递 lambda 非常简单:

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

但我想知道,将 lambda 传递给这样的函数的成本是多少?如果 func 将 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“代码体”和一组捕获的变量?代码体是否有很多多余的副本?我是否必须标记每个function传递的对象const&以免复制:

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

或者函数对象的传递方式是否与常规 C++ 结构不同?

4

3 回答 3

34

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

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


拉姆达斯

实现 lambda 最直接的方法是一种 unnamed 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

这几乎就是 lambdas 本身的全部内容(除了我省略的少数实现细节,但与理解它的工作原理无关)。


std::function

std::function是围绕任何类型的仿函数(lambdas、独立/静态/成员函数、像我展示的仿函数类......)的通用包装器。

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

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

或者,

  • 指向函子副本的指针[见下面的注释](动态分配以允许任何类型的函子,正如您正确指出的那样)。
  • 指向要调用的成员函数的指针。
  • 一个指向分配器的指针,它能够复制仿函数和它本身(因为可以使用任何类型的仿函数,所以应该使用指向仿函数的指针void*,因此必须有这样的机制——可能使用多态性。base类 + 虚拟方法,派生类在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

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


回答问题

将 lambda 传递给这样的函数的成本是多少?[问题的背景:按价值]

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

我是否必须标记每个传递的函数对象const&以免复制?

恐怕这很难以通用的方式回答。有时您希望通过const引用传递,有时通过值传递,有时通过右值引用传递,以便您可以移动它。这实际上取决于代码的语义。

关于你应该选择哪一个的规则是一个完全不同的主题 IMO,只要记住它们与任何其他对象相同。

无论如何,您现在拥有做出明智决定的所有关键(同样,取决于您的代码及其语义)。

于 2013-06-04T15:45:36.367 回答
4

另请参见C++11 lambda 实现和内存模型

lambda 表达式就是这样:一个表达式。编译后,它会在运行时生成一个闭包对象。

5.1.2 Lambda 表达式 [expr.prim.lambda]

对 lambda 表达式的求值会产生一个临时纯右值 (12.2)。这个临时对象称为闭包对象。

对象本身是实现定义的,并且可能因编译器而异。

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

于 2013-06-04T15:50:22.073 回答
1

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

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

于 2013-06-04T15:43:53.673 回答