std :: function是如何实现的?
根据我发现的来源, lambdaexpression式本质上是由编译器创build一个重载的函数调用操作符和引用的variables作为成员的类来实现的。 这表明lambdaexpression式的大小是变化的,并给出足够大的引用variables,这个variables的大小可以是任意大的 。
一个std::function
应该有一个固定的大小 ,但它必须能够包装任何种类的可调用,包括任何types的lambda。 它是如何实现的? 如果std::function
内部使用一个指向它的目标的指针,那么当std::function
实例被复制或移动时会发生什么? 有涉及堆分配吗?
std::function
的实现可以从一个实现到另一个实现有所不同,但其核心思想是它使用了type-erasure。 虽然有多种方法可以做到这一点,但您可以想象一个微不足道的(不是最优的)解决scheme可能是这样的(为简单起见,简化了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
存储到一个基types中。 对于函数使用的每个不同的函子,都会创build一个从该基类派生的新types,并且该types的一个对象将dynamic地实例化。 std::function
对象的大小始终相同,并且会根据堆中不同函数的需要分配空间。
在现实生活中有不同的优化提供性能优势,但会使答案复杂化。 该types可以使用小对象优化,dynamic调度可以被一个自由函数指针所替代,该指针将函数作为参数来避免一个间接级别…但是这个想法基本上是相同的。
关于如何复制std::function
,快速testing表明内部可调用对象的拷贝已经完成,而不是共享状态。
// 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) }
testing表明f2
获取可调用实体的副本,而不是参考。 如果可调用的实体被不同的std::function<>
对象共享,程序的输出将是5,6,7。
对于某些types的参数(“如果f的目标是通过reference_wrapper
或函数指针传递的可调用对象), std::function
的构造函数不允许任何exception,所以使用dynamic内存是不可能的。 对于这种情况,所有的数据都必须直接存储在std::function
对象中。
在一般情况下(包括lambda情况),使用dynamic内存(通过标准分配器或传递给std::function
构造std::function
的分配器)是允许的,因为实现看起来合适。 标准build议实现不使用dynamic内存,如果可以避免的话,但正如你所说的,如果函数对象(不是std::function
对象,而是包含在它内部的对象)足够大,那么就没有办法以防止它,因为std::function
具有固定的大小。
这个抛出exception的权限被授予正常的构造函数和复制构造函数,它们在复制过程中也明确地允许dynamic内存分配。 对于移动,没有理由为什么dynamic内存是必要的。 标准似乎没有明确地禁止它,如果移动可能会调用包装对象types的移动构造函数,可能不会,但是您应该能够假设如果实现和对象都是合理的,移动不会导致任何分配。
来自@DavidRodríguez的解答 – dribeas对于展示types擦除很好,但是不够好,因为types擦除还包括如何复制types(在该答案中函数对象将不可复制)。 这些行为也存储在function
对象中,除了函子数据。
从Ubuntu 14.04 gcc 4.8开始,在STL实现中使用的技巧是编写一个通用函数,使用每个可能的函子types对其进行专门化,并将其转换为通用函数指针types。 因此,types信息被擦除 。
我修补了一个简化的版本。 希望它会有所帮助
#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实现不是最好的解决scheme,因为我听说过一些更快的实现 。 不过我相信这个机制是一样的。
一个std::function
重载operator()
使其成为一个函数对象,lambda的工作方式是一样的。 它基本上创build一个结构与成员variables,可以在operator()
函数内访问。 所以要记住的基本概念是lambda是一个对象(称为函子或函数对象)而不是函数。 标准说如果可以避免的话,不要使用dynamic内存。