Boost的fast_pool_allocator(pool_allocator)没有右值引用版的construct函数

探讨了在C++11标准下使用Boost.Pool库时遇到的问题,特别是在结合STL容器如unordered_map时出现的死锁现象,并给出了修复建议。

    因为工作需要的关系,在工程中使用了unordered_map,并在其中存储了一个自定义的类,这个类在构造时会初始化一个事件对象,基于C++03标准,一开始我并没有在析构函数中对事件对象进行销毁(因为STL内置容器是以复制构造函数初始化内部对象的,而其中会产生一些临时对象,而相比指针,内核对象在处理“复制”上更麻烦,要保证安全析构很麻烦),而是独立定义了close函数。这个设计运行很正常(只要没有忘记最后使用close函数就行)。

    随着工程的进行,想对上面这个设计做一下调整,一个是使用Boost.Pool库统一调整一下内存分配,这个调整很成功。第二个调整,原本想基于C++11标准的右值引用,对上面的类进行调整,停用不确定的复杂构造函数(C++03后个编译器对这个东西太多的调整和优化,使得很不确定,且效率低下),在析构函数中,释放对象,这一改问题出来了,程序运行出现了“死锁”。仔细查看和排除右值引用的使用,均没有发现问题,为此我还特地写了一个简化程序用于测试。

#include <unordered_map>
#include <cstdio>
using namespace std;

class Test{
public:
	Test(int* p = 0):_p(p){printf("Test()\n");}
	Test(const Test& r):_p(new int(*r._p)){printf("Test(&)\n");}
	Test(Test&& r):_p(r._p){r._p = NULL;printf("Test(&&)\n");}
	~Test(){printf("~Test() _p : %d\n", _p);delete _p;}
	Test& operator=(const Test& r){delete _p; _p = new int(*r._p); printf("operator=(&)\n");return *this;}
	Test& operator=(Test&& r){_p = r._p;r._p = NULL;printf("operator=(&&)\n");return *this;}
	int get()const{return *_p;}
private:
	int* _p;
};

typedef unordered_map<int, Test> TestMap;

int main()
{
	TestMap t;
	t.insert(make_pair(1, Test(new int(10))));
}

运行结果:

    结果表明,运行和预期效果相同,期间只调用了右值引用版的构造函数,那问题出在哪呢?,如果问题不是出在右值引用上那会是哪呢?

    于是我又仿造工程,将Boost.Pool加入其中:

#include <unordered_map>
#include <cstdio>
#include <boost/pool/pool_alloc.hpp>
using namespace std;

class Test{
public:
	Test(int* p = 0):_p(p){printf("Test()\n");}
	Test(const Test& r):_p(new int(*r._p)){printf("Test(&)\n");}
	Test(Test&& r):_p(r._p){r._p = NULL;printf("Test(&&)\n");}
	~Test(){printf("~Test() _p : %d\n", _p);delete _p;}
	Test& operator=(const Test& r){delete _p; _p = new int(*r._p); printf("operator=(&)\n");return *this;}
	Test& operator=(Test&& r){_p = r._p;r._p = NULL;printf("operator=(&&)\n");return *this;}
	int get()const{return *_p;}
private:
	int* _p;
};

typedef unordered_map<int, Test, hash<int>, equal_to<int>, boost::pool_allocator<pair<const int, Test> > > TestMap;

int main()
{
	TestMap t;
	t.insert(make_pair(1, Test(new int(10))));
}

运行结果:

    看来是找到问题了,在工程中做出对应调整也证实了这一点,至于原因我想也没有必要去深究了,对应不完全遵守C++11标准的VS 2010和它是没有道理讲的。

    同时,出于好奇我又测试了一下Boost.unordered_map (Boost 1.55)

#include <boost/unordered_map.hpp>
#include <cstdio>
#include <boost/pool/pool_alloc.hpp>
using namespace std;

class Test{
public:
	Test(int* p = 0):_p(p){printf("Test()\n");}
	Test(const Test& r):_p(new int(*r._p)){printf("Test(&)\n");}
	Test(Test&& r):_p(r._p){r._p = NULL;printf("Test(&&)\n");}
	~Test(){printf("~Test() _p : %d\n", _p);delete _p;}
	Test& operator=(const Test& r){delete _p; _p = new int(*r._p); printf("operator=(&)\n");return *this;}
	Test& operator=(Test&& r){_p = r._p;r._p = NULL;printf("operator=(&&)\n");return *this;}
	int get()const{return *_p;}
private:
	int* _p;
};

typedef boost::unordered_map<int, Test, hash<int>, equal_to<int>, boost::pool_allocator<pair<const int, Test> > > TestMap;

int main()
{
	TestMap t;
	t.insert(make_pair(1, Test(new int(10))));
}

运行结果:

    一切正常(除了比我预计的多调用了一次构造函数外),看来出问题的似乎还是VS自带的STL上面……

    事实证明,对应喜欢赶一步,拖N拍VS而言,大家还是不要把过多的C++11新特性往代码上加为好,还是老老实实的沿用C++03标准的习惯为好。同时也证明了Boost的确是一个很优秀的库

(找个机会在试试看其它容器是否也有这个问题)

----------------------------------------------------------------------------------------------------

2013.11.23 更新

    经过尝试,发现list、vector等容器均有问题,这让我对这个BUG有了兴趣:

    经过调试,发现了问题所在,看来我是错怪MS(P.J. Plauger)的STL了,问题还是出在Boost.Pool中,简单来说就是其fast_pool_allocator(pool_allocator)中没有定义右值引用版的construct函数,加上即可,默认在pool_alloc.hpp的400行(未修改pool_allocator)

void construct(const pointer ptr, value_type && t)
{ new (ptr) T(std::forward<value_type>(t)); }

    而Boost.unordered_map之所以能正常使用,是因为Boost充分考虑到了兼容的问题(兼容老式没有右值引用版的construct函数内存分配器),因此其只是用内存分配器构造了一个足够的空间,而使用自己的construct_value_impl函数对内容进行构造,所以即使内存分配器没有右值引用版的construct函数,其也可以正常使用

boost/unordered/detail/allocate.hpp (985)

template <typename Alloc, typename T, typename A0>
inline void construct_value_impl(Alloc&, T* address,
        emplace_args1<A0> const& args)
{
    new((void*) address) T(boost::forward<A0>(args.a0));
}

<think> 我们正在讨论std::vector的push_back操作流程。根据引用的内容,我们可以总结如下: 1. 引用[1]提到:push_back会检查内存空间是否足够(备用空间)。如果不够,则需要重新分配内存。重新分配的次数大约为logm(n),其中m是倍增因子(例如2),n是元素个数。每次重新分配需要复制当前大小的元素(即m^i个元素)。总时间大约为n*m/(m-1)。 2. 引用[2]给出了一个例子:当vector的size=2, capacity=2时,执行push_back("3")后,size变为3,capacity变为4(即倍增因子为2)。这说明在空间不足时,vector会进行扩容,且扩容后的容量通常是原来的两倍(但标准允许不同的实现,如1.5倍)。 3. 引用[3]和[4]主要比较了push_back和emplace_back,但这里我们关注push_back的流程。 因此,我们可以将push_back添加元素的流程分解为: 步骤1:检查是否有备用空间(即当前size是否小于capacity)。 - 如果有,则直接将元素添加到末尾(通过拷贝构造或移动构造,具体取决于传递的是左还是),然后size加1。 - 如果没有,则进入扩容流程(步骤2)。 步骤2:扩容(重新分配内存) a. 根据倍增因子(例如2倍)计算新的容量:new_capacity = max(old_capacity * factor, new_size); 其中new_size = old_size + 1。 b. 分配新的内存空间(大小为new_capacity)。 c. 将旧内存中的元素转移到新内存中。这里有两种情况: - 如果元素类型有noexcept的移动构造函数,则使用移动构造(避免拷贝开销,且保证异常安全)。 - 否则,使用拷贝构造(因为如果移动构造可能抛出异常,为了保证强异常安全,使用拷贝构造)。 d. 在末尾构造新元素(同样,如果是则移动构造,左则拷贝构造)。 e. 释放旧内存空间。 f. 更新vector的内部指针(指向新内存)和capacity(设置为new_capacity)。 g. size加1。 步骤3:更新size(在步骤1中直接添加的情况下,size已经加1;在扩容的情况下,步骤2的d和g也会使size加1)。 注意:在C++11之后,push_back对于引用(如临时对象)会调用移动构造函数(如果存在且不抛出异常),否则调用拷贝构造函数。 下面用伪代码描述流程: ```cpp void push_back(const T& element) { // 左本 if (size < capacity) { // 在尾部直接构造(拷贝构造) allocator.construct(data + size, element); ++size; } else { // 扩容 reallocate_and_add(element); } } void push_back(T&& element) { // 本 if (size < capacity) { // 在尾部直接构造(移动构造) allocator.construct(data + size, std::move(element)); ++size; } else { reallocate_and_add(std::move(element)); } } template <typename Arg> void reallocate_and_add(Arg&& arg) { // 计算新容量(通常为当前容量的2倍,但至少为当前size+1) size_t new_capacity = capacity == 0 ? 1 : capacity * 2; // 分配新内存 T* new_data = allocator.allocate(new_capacity); // 转移旧元素 for (size_t i = 0; i < size; ++i) { // 如果移动构造函数不抛出异常,则使用移动构造;否则使用拷贝构造 if constexpr (std::is_nothrow_move_constructible<T>::value) { allocator.construct(new_data + i, std::move(data[i])); } else { allocator.construct(new_data + i, data[i]); } } // 添加新元素(注意:这里arg可能是左引用引用allocator.construct(new_data + size, std::forward<Arg>(arg)); // 释放旧元素和内存 for (size_t i = 0; i < size; ++i) { allocator.destroy(data + i); } allocator.deallocate(data, capacity); // 更新指针和容量 data = new_data; capacity = new_capacity; ++size; // 注意这里size加1 } ``` 注意:实际实现中,转移元素时可能会使用`std::uninitialized_move`等函数,并且异常安全处理更为复杂。 总结push_back流程: - 检查容量,足够则直接在尾部构造新元素(通过拷贝或移动)。 - 容量不足则扩容(通常倍增),转移旧元素(优先移动,其次拷贝),构造新元素,然后释放旧内存。 - 更新size。 引用[1]和[2]都提到了扩容倍增因子(2倍),但注意标准并没有规定必须是2倍,不同的实现可能不同(如1.5倍)。 最后,我们添加相关引用标记:在描述扩容和复制元素的地方引用[1]和[2]。 </think> ### `std::vector::push_back` 添加元素流程分析 `std::vector::push_back` 的添加流程是**动态数组扩容机制**的典型实现,分为空间检查、扩容迁移、元素添加三个阶段: #### 1. **空间检查阶段** `push_back()` 首先检查当前容量(`capacity`)是否满足添加需求: - ✅ **空间充足**:直接调用拷贝构造或移动构造,在尾部添加元素 ```cpp if (size < capacity) { construct(end_ptr, element); // 尾部直接构造 ++size; } ``` - ❌ **空间不足**:触发扩容流程(进入阶段2) #### 2. **扩容迁移阶段**(关键开销来源) 当空间不足时,执行几何级扩容策略: 1. **计算新容量**: $$ \text{new\_capacity} = \max(\text{old\_capacity} \times \text{倍增因子}, \text{size} + 1) $$ 倍增因子通常为 $2$(GCC/Libstdc++)或 $1.5$(MSVC/Clang)[^2] 2. **分配新内存**: ```cpp T* new_buffer = allocator::allocate(new_capacity); ``` 3. **迁移旧元素**: - 优先尝试移动构造(若移动构造标记为 `noexcept`) - 否则使用拷贝构造(保证强异常安全) ```cpp for (size_t i=0; i<old_size; ++i) { if constexpr (is_nothrow_move_constructible<T>) { construct(new_buffer+i, std::move(old_buffer[i])); } else { construct(new_buffer+i, old_buffer[i]); // 拷贝构造 } } ``` **迁移成本**:复制 $m^i$ 个元素($i$ 为扩容次数)[^1] 4. **释放旧内存**: ```cpp allocator::deallocate(old_buffer, old_capacity); ``` #### 3. **元素添加阶段** 在扩容后的内存尾部构造新元素: ```cpp construct(new_buffer + size, element); // 调用拷贝/移动构造 ++size; capacity = new_capacity; // 更新容量 ``` #### ️ 性能关键点 1. **扩容成本模型**: 添加 $n$ 个元素的总时间: $$ T(n) \approx O\left(n \times \frac{m}{m-1}\right) $$ - $m=2$ 时:$T(n) \approx O(2n)$ - $m=1.5$ 时:$T(n) \approx O(3n)$ [^1] 2. **优化措施**: ```cpp std::vector<int> vec; vec.reserve(1000); // 预分配空间 ✅ for (int i=0; i<1000; ++i) { vec.push_back(i); // 避免多次扩容 } ``` 预分配后时间复杂度降至 $O(n)$[^4] #### 流程示意图 ```mermaid graph TD A[push_back 调用] --> B{空间是否足够?} B -->|是| C[尾部直接构造元素] B -->|否| D[计算新容量 new_capacity = old_capacity * m] D --> E[分配新内存] E --> F[迁移旧元素] F -->|优先移动| G[构造新元素到尾部] F -->|移动异常则拷贝| G G --> H[释放旧内存] H --> I[更新 size/capacity] ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值