根据我发现的消息来源,lambda表达式实际上是由编译器实现的,该编译器创建了一个带有重载函数调用运算符并将引用变量作为成员的类。这表明lambda表达式的大小是变化的,并且给定足够多的引用变量,大小可能会任意大。
的大小std::function
应为固定,但必须能够包装任何类型的可调用对象,包括任何同类型的lambda。如何实施?如果std::function
内部使用指向其目标的指针,那么在std::function
复制或移动实例时会发生什么?是否涉及任何堆分配?
根据我发现的消息来源,lambda表达式实际上是由编译器实现的,该编译器创建了一个带有重载函数调用运算符并将引用变量作为成员的类。这表明lambda表达式的大小是变化的,并且给定足够多的引用变量,大小可能会任意大。
的大小std::function
应为固定,但必须能够包装任何类型的可调用对象,包括任何同类型的lambda。如何实施?如果std::function
内部使用指向其目标的指针,那么在std::function
复制或移动实例时会发生什么?是否涉及任何堆分配?
Answers:
std::function
一个实现与另一个实现可能有所不同,但是核心思想是它使用类型擦除。尽管有多种方法可以做到,但您可以想象一个简单的(不是最佳的)解决方案可能是这样的(为std::function<int (double)>
简单起见,为特定情况进行了简化):
struct callable_base {
virtual int operator()(double d) = 0;
virtual ~callable_base() {}
};
template <typename F>
struct callable : callable_base {
F functor;
callable(F functor) : functor(functor) {}
virtual int operator()(double d) { return functor(d); }
};
class function_int_double {
std::unique_ptr<callable_base> c;
public:
template <typename F>
function(F f) {
c.reset(new callable<F>(f));
}
int operator()(double d) { return c(d); }
// ...
};
用这种简单的方法 function
对象仅unique_ptr
将a 存储为基本类型。对于与一起使用的每个不同的函子,将function
创建一个从基派生的新类型,并动态实例化该类型的对象。该std::function
对象始终具有相同的大小,并将根据需要为堆中的不同函子分配空间。
在现实生活中,有多种优化措施可提供性能优势,但使答案复杂化。该类型可以使用小型对象优化,可以将动态分配替换为以函子为参数的自由函数指针,以避免一个间接级别……但是思想基本相同。
关于如何复制 std::function
行为行为,快速测试表明内部可调用对象的副本已完成,而不是共享状态。
// g++4.8
int main() {
int value = 5;
typedef std::function<void()> fun;
fun f1 = [=]() mutable { std::cout << value++ << '\n' };
fun f2 = f1;
f1(); // prints 5
fun f3 = f1;
f2(); // prints 5
f3(); // prints 6 (copy after first increment)
}
测试表明f2
获取了可调用实体的副本,而不是引用。如果可调用实体由不同的std::function<>
对象共享,则程序的输出将为5、6、7。
std::function
如果复制内部对象,的语义是否正确,我认为情况并非如此(想了拉姆达捕获的值,是可变的,里面存放的一个std::function
,如果功能状态被复制的拷贝数std::function
这是不希望内部的标准算法可能会导致不同的结果。
std::function
将触发分配。
@DavidRodríguez的答案-dribeas很好地证明了类型擦除,但效果还不够好,因为类型擦除还包括如何复制类型(在该答案中,功能对象将不可复制构造)。这些行为function
除了函子数据外,还存储在对象中。
在Ubuntu 14.04 gcc 4.8的STL实现中使用的技巧是编写一个泛型函数,使用每种可能的函子类型对其进行专用化,然后将其转换为通用函数指针类型。因此,类型信息被擦除。
我已经整理了一个简化的版本。希望对你有帮助
#include <iostream>
#include <memory>
template <typename T>
class function;
template <typename R, typename... Args>
class function<R(Args...)>
{
// function pointer types for the type-erasure behaviors
// all these char* parameters are actually casted from some functor type
typedef R (*invoke_fn_t)(char*, Args&&...);
typedef void (*construct_fn_t)(char*, char*);
typedef void (*destroy_fn_t)(char*);
// type-aware generic functions for invoking
// the specialization of these functions won't be capable with
// the above function pointer types, so we need some cast
template <typename Functor>
static R invoke_fn(Functor* fn, Args&&... args)
{
return (*fn)(std::forward<Args>(args)...);
}
template <typename Functor>
static void construct_fn(Functor* construct_dst, Functor* construct_src)
{
// the functor type must be copy-constructible
new (construct_dst) Functor(*construct_src);
}
template <typename Functor>
static void destroy_fn(Functor* f)
{
f->~Functor();
}
// these pointers are storing behaviors
invoke_fn_t invoke_f;
construct_fn_t construct_f;
destroy_fn_t destroy_f;
// erase the type of any functor and store it into a char*
// so the storage size should be obtained as well
std::unique_ptr<char[]> data_ptr;
size_t data_size;
public:
function()
: invoke_f(nullptr)
, construct_f(nullptr)
, destroy_f(nullptr)
, data_ptr(nullptr)
, data_size(0)
{}
// construct from any functor type
template <typename Functor>
function(Functor f)
// specialize functions and erase their type info by casting
: invoke_f(reinterpret_cast<invoke_fn_t>(invoke_fn<Functor>))
, construct_f(reinterpret_cast<construct_fn_t>(construct_fn<Functor>))
, destroy_f(reinterpret_cast<destroy_fn_t>(destroy_fn<Functor>))
, data_ptr(new char[sizeof(Functor)])
, data_size(sizeof(Functor))
{
// copy the functor to internal storage
this->construct_f(this->data_ptr.get(), reinterpret_cast<char*>(&f));
}
// copy constructor
function(function const& rhs)
: invoke_f(rhs.invoke_f)
, construct_f(rhs.construct_f)
, destroy_f(rhs.destroy_f)
, data_size(rhs.data_size)
{
if (this->invoke_f) {
// when the source is not a null function, copy its internal functor
this->data_ptr.reset(new char[this->data_size]);
this->construct_f(this->data_ptr.get(), rhs.data_ptr.get());
}
}
~function()
{
if (data_ptr != nullptr) {
this->destroy_f(this->data_ptr.get());
}
}
// other constructors, from nullptr, from function pointers
R operator()(Args&&... args)
{
return this->invoke_f(this->data_ptr.get(), std::forward<Args>(args)...);
}
};
// examples
int main()
{
int i = 0;
auto fn = [i](std::string const& s) mutable
{
std::cout << ++i << ". " << s << std::endl;
};
fn("first"); // 1. first
fn("second"); // 2. second
// construct from lambda
::function<void(std::string const&)> f(fn);
f("third"); // 3. third
// copy from another function
::function<void(std::string const&)> g(f);
f("forth - f"); // 4. forth - f
g("forth - g"); // 4. forth - g
// capture and copy non-trivial types like std::string
std::string x("xxxx");
::function<void()> h([x]() { std::cout << x << std::endl; });
h();
::function<void()> k(h);
k();
return 0;
}
STL版本中也有一些优化
construct_f
与destroy_f
混合成一个函数指针(以告诉做什么额外的参数),以节省一些字节union
,因此,当function
从函数指针构造对象时,它将直接存储在union
而不是堆空间中也许STL的实施不是最好的解决方案,因为我听说一些更快的实施。但是,我认为基本机制是相同的。
对于某些类型的参数(“如果f的目标是通过它传递的可调用对象reference_wrapper
或函数指针”),std::function
的构造函数不允许出现任何异常,因此使用动态内存是不可能的。在这种情况下,所有数据都必须直接存储在std::function
对象。
在一般情况下(包括lambda情况),std::function
允许使用动态内存(通过标准分配器或传递给构造函数的分配器)以实现合适的实现。该标准建议,如果可以避免的话,请不要使用动态内存,但是正如您正确地说的那样,如果函数对象(不是std::function
对象,而是包裹在其中的对象)足够大,则无法阻止它,以来std::function
尺寸固定
普通构造函数和复制构造函数均具有这种引发异常的权限,它们也明确地允许在复制期间进行动态内存分配。对于移动,没有理由需要动态内存。该标准似乎没有明确禁止它,并且如果移动可能调用包装对象的类型的move构造函数,则可能不会,但是您应该能够假定,如果实现和您的对象都是明智的,则移动不会导致任何分配。
一个std::function
过载operator()
使其成为一个仿函数对象,拉姆达的工作方式相同。它基本上会创建一个带有成员变量的结构,该成员变量可以在operator()
函数内部访问。因此要记住的基本概念是,lambda是对象(称为函子或函数对象)而不是函数。该标准说,如果可以避免,则不要使用动态内存。
std::function
?这是这里的关键问题。
std::function
对象的大小都是相同的,并且不是所包含的lambda的大小。
std::vector<T...>
对象都有一个(copiletime)固定大小,而与实际分配器实例/元素数无关。
std::function<void ()> f;
无需在那里分配,std::function<void ()> f = [&]() { /* captures tons of variables */ };
很可能是在分配。std::function<void()> f = &free_function;
可能也不会分配...
std::function
一段时间研究了gcc / stdlib的实现。本质上,它是多态对象的句柄类。创建内部基类的派生类以保存分配在堆上的参数-然后,指向该对象的指针作为的子对象保存std::function
。我相信它使用引用计数std::shared_ptr
来处理复制和移动。