std::function 的作用很强大,他让vector等保存不同类型函数的对象: function, functor, lambda…成为了可能。但他有一个潜在的性能风险:在保存lambda/bind时,如果对象的大小 大于两个指针的大小,他就需要分配动态空间。这是大部分人写C++的人不期望看到的。
先看function实现的几段代码:6.1.0a-2/include/c++/6.1.0/functional
void* _M_access() { return &_M_pod_data[0]; }
const void* _M_access() const { return &_M_pod_data[0]; }
template
_Tp&
_M_access()
{ return *static_cast<_Tp*>(_M_access()); }
1651 _M_init_functor(_Any_data& __functor, _Functor&& __f, true_type)
1652 { new (__functor._M_access()) _Functor(std::move(__f)); }
1653
1654 static void
1655 _M_init_functor(_Any_data& __functor, _Functor&& __f, false_type)
1656 { __functor._M_access<_Functor*>() = new _Functor(std::move(__f)); }
初始化的时候,重载了两个函数,决定是用_M_pod_data的空间作为存储空间,还是作为指针,指向分配的地址。
问题是,怎么决定调哪一个函数呢?看下面的代码:
1625 static void
1626 _M_init_functor(_Any_data& __functor, _Functor&& __f)
1627 { _M_init_functor(__functor, std::move(__f), _Local_storage()); }
调用哪个函数由_Local_storage决定.
1538 class _Function_base
1539 {
1540 public:
1541 static const std::size_t _M_max_size = sizeof(_Nocopy_types);
1542 static const std::size_t _M_max_align = __alignof__(_Nocopy_types);
1543
1544 template
1545 class _Base_manager
1546 {
1547 protected:
1548 static const bool __stored_locally =
1549 (__is_location_invariant<_Functor>::value
1550 && sizeof(_Functor) <= _M_max_size
1551 && __alignof__(_Functor) <= _M_max_align
1552 && (_M_max_align % __alignof__(_Functor) == 0));
1553
1554 typedef integral_constant _Local_storage;
local storage的true或者false由__stored_locally决定。__stored_locally的大小决定中有一条:sizeof(_Functor) <= _M_max_size;关键看Functor的大小是否小于_M_max_size,_M_max_size的大小等于_Nocopy_types。接着看_Nocopy_types的实现:
1457 union _Nocopy_types
1458 {
1459 void* _M_object;
1460 const void* _M_const_object;
1461 void (*_M_function_pointer)();
1462 void (_Undefined_class::*_M_member_pointer)();
1463 };
咋一看,这个union的大小应该是一个指针的大小,里面有个关键成员:void (_Undefined_class:?_M_member_pointer)(); 指向成员函数的指针,指向成员函数的指针的大小是2个指针的大小。所以结果就是:如果传入functor的大小大于两个指针的大小,就会分配动态内存。函数指针的大小就是一个指针的大小,那什么时候functor会大于一个指针的大小:lambda和bind。我们以lambda为例来看。bind和lambda的效果一样。
编译器在碰到lambda函数时,事实上,会生成一个对象来保存lambda函数。这个对象重载了operator(),所以能像函数一样调用。同时,这个对象还有数据成员,这些数据成员就是捕获来的对象。譬如:
auto lf = [&a, &b]{ return 0;};
std::cout << sizeof(lf) << std::endl;
``
这个对象捕获了两个引用(相当于两个指针的大小),所以输出就是16.
# 类成员函数的指针的大小
前面提到类成员函数的指针大小是两个指针的大小。为什么? 因为类成员函数的指针还有一个偏移量, 这个类相对于基类指针的偏移 。
考虑三个类:
```C++
#include
#include
class A{
public:
int getA() {
std::cout << "A address: " << this << "\n";
return a;
}
private:
int a{1};
};
class B{
public:
int getB() {
std::cout << "B address: " << this << "\n";
return a;
}
private:
int a{2};
};
class Derived: public A, public B {
};
我们用Derived的对象去访问函数getA期望返回的是class A的a, 如果访问getB期望返回的是class B的a;
Derived d;
std::cout << "getA: " << d.getA() << ", getB: " << d.getB() << std::endl;
输出是:
B address: 0x7ffe00762494
A address: 0x7ffe00762490
getA: 1, getB: 2
访问B的时候,this指针做了偏移,指向了class B在class D中的开始地址。假如使用成员函数指针,这个偏移量,只有在成员函数指针被赋值的时候才能知道。
考虑函数:
void print(Derived & d, int (Derived::*p_mem)())
{
void *data[2];
std::memcpy(data, &p_mem, sizeof(p_mem));
std::cout << "print --------\n"
<< "object ptr: " << &d << "\n"
<< "function ptr: " << data[0] << "\n"
<< "point adj: " << data[1] << "\n"
<< "value: " << (d.*p_mem)() << std::endl;
}
print(d, &Derived::getA);
print(d, &Derived::getB);
的输出就是:
A address: 0x7ffc8164e770
print --------
object ptr: 0x7ffc8164e770
function ptr: 0x400b82
point adj: 0
value: 1
B address: 0x7ffc8164e774
print --------
object ptr: 0x7ffc8164e770
function ptr: 0x400bc4
point adj: 0x4
value: 2
参考文章:http://lazarenko.me/wide-pointers/
如果用std::function的变量来保存lambda或bind的函数对象时,需要格外注意,如果对象的大小大于两个指针的大小,就会发生动态内存分配,影响性能。