C++ STL:为何分配器不会增加容器的内存占用,而比较器会?
先看你给出的代码运行结果:
#include <functional> #include <iostream> #include <memory> #include <set> struct MyLess : public std::less<int> { char dummy[1024]; }; struct MyAllocator : public std::allocator<int> { char dummy[1024]; }; int main() { std::cout << sizeof(std::set<int, MyLess>) << std::endl; // prints 1064 std::cout << sizeof(std::set<int, std::less<int>, MyAllocator>) << std::endl; // prints 48 return 0; }
这个差异其实是GCC的STL实现对比较器和分配器的存储策略不同,再加上分配器的rebind机制细节共同导致的,我给你拆解一下:
1. 比较器是容器实打实的成员变量
GCC里std::set底层靠红黑树(_Rb_tree)实现,红黑树会把比较器作为直接成员变量存在容器对象里。你写的MyLess虽然继承了空的std::less<int>,但加了1024字节的dummy数组,所以它本身就占1024字节(加上空类的对齐要求,最终刚好对齐到8字节边界)。这个大小会直接加到std::set原本的成员大小上(比如红黑树的根指针、节点计数这些,加起来大概40字节),1024+40=1064,正好是你看到的结果。
2. 分配器是基类,还因rebind机制“偷偷”用了空类
和比较器不一样,GCC的红黑树并没有把分配器当成员变量存,而是继承自rebind之后的节点分配器,这里用到了C++的空基类优化(EBO)——如果基类是空类,就不会额外占内存。
但这里有个容易忽略的点:你的MyAllocator只继承了std::allocator<int>,却没重写rebind模板。当红黑树需要为它的节点分配内存时,会通过allocator_traits调用分配器的rebind方法,而因为MyAllocator没自己定义rebind,就会用父类std::allocator<int>的版本,返回的是std::allocator<_Rb_tree_node<int>>——这可是个空类!
所以红黑树实际继承的是空类,空基类优化生效,自然不会增加容器的大小,std::set的大小还是原本的48字节。要是你把MyAllocator改成模板类,并重写rebind让它给节点类型返回带1024字节的分配器,那容器大小肯定会涨上去。
内容的提问来源于stack exchange,提问作者Leon Cruz

