[英]How are C++11 lambdas represented and passed?
在 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“代码体”和捕获的变量组是如何传递的? 代码体是否有很多多余的副本? 我是否必须标记通过const&
传递的每个function
对象const&
以便不制作副本:
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)
构造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
functor 相比,开销可以忽略不计,但它当然比按引用传递struct
functor 高得多。
我是否必须标记通过
const&
传递的每个函数对象const&
以便不制作副本?
恐怕这很难以通用的方式回答。 有时您会希望通过const
引用、有时通过值、有时通过右值引用传递,以便您可以移动它。 这实际上取决于您的代码的语义。
关于您应该选择哪一个的规则是一个完全不同的主题 IMO,请记住它们与任何其他对象相同。
无论如何,您现在拥有做出明智决定的所有关键(同样,取决于您的代码及其语义)。
一个 lambda 表达式就是:一个表达式。 编译后,它会在运行时生成一个闭包对象。
5.1.2 Lambda 表达式 [expr.prim.lambda]
对 lambda 表达式的求值产生一个 prvalue 临时值 (12.2)。 这个临时对象称为闭包对象。
对象本身是实现定义的,可能因编译器而异。
这是在 clang https://github.com/faisalv/clang-glambda中 lambdas 的原始实现
如果 lambda 可以作为一个简单的函数(即它不捕获任何东西),那么它的制作方式完全相同。 特别是标准要求它与具有相同签名的旧式函数指针兼容。 [编辑:它不准确,请参阅评论中的讨论]
其余的取决于实施,但我不担心。 最直接的实现只是携带信息。 与您在捕获中要求的一样多。 所以效果和你手动创建一个类是一样的。 或者使用一些 std::bind 变体。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.