免责声明:与现实相比,我的回答有些简化(我把一些细节放在一边),但大局就在这里。此外,标准没有完全指定 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,只要记住它们与任何其他对象相同。
无论如何,您现在拥有做出明智决定的所有关键(同样,取决于您的代码及其语义)。