boost中的指针容器
基本概念
在C++中用容器来管理动态分配的内存,首先想到是会通过标准容器来存储shared_ptr或者unique_ptr,如下代码:
#include <memory>
#include <vector>
int main()
{
std::vector<std::shared_ptr<int> > v;
v.push_back(std::hared_ptr<int>(new int(1)));
v.push_back(std::shared_ptr<int>(new int(2)));
}
指针容器
boost库中专门提供了指针容器存储指针来管理动态分配的对象。标准库中vector,list,deque分别有ptr_vector,ptr_list,ptr_deque相对应,并且接口也与标准库中的容器一致。指针容器与容器+unique_ptr来管理动态分配的内存的语义是一致,对容器成员都是独占语义,不可复制,只可通过移动来转移所有权。不同的是指针容器直接操作的是裸指针。
具体可以看 boost文档说明
使用方法
指针容器与原容器的接口一直,如下示例
#include <iostream>
#include <boost/ptr_container/ptr_container.hpp>
int main()
{
//以ptr_vector为例
boost::ptr_vector<int> ints;
//插入元素
ints.push_back(new int(18));
ints.push_back(new int(118));
//以下标取元素
std::cout << ints[0] << std::endl;
//通过迭代器取元素
std::cout << (*(ints.begin())) << std::endl;
std::system("pause");
}
指针容器的内存释放
在指针容器会在析构时自动释放元素的内存,所以无需再额外的迭代释放每个成员的内存,如下:
#include <boost/ptr_container/ptr_container.hpp>
#include <iostream>
#define _crtdbg_map_alloc
class CTest
{
public:
CTest(int i):i(i)
{
std::cout <<"construct"<<std::endl;
}
~CTest()
{
std::cout << "construct" << std::endl;
}
int i;
};
int main()
{
{
boost::ptr_vector<CTest> Tests;
Tests.push_back(new CTest(1));
Tests.push_back(new CTest(2));
}
//system("pause");
_CrtDumpMemoryLeaks();
return 0;
}
运行可以看到是没有内存泄漏的。
容器成员的所有权
在容器析构时是会释放所有成员的内存。那么直接获取容器中的成员,就会涉及到所有权问题(谁来释放内存)。如果不注意就会造成重复释放内存的问题,如下示例代码:
int main()
{
CTest *p = NULL;
{
boost::ptr_vector<CTest> Tests;
Tests.push_back(new CTest(1));
p = &Tests.front();
}
delete p;
p = NULL;
system("pause");
return 0;
}
上面的代码会直接崩溃,是因为重复释放了p指针所指的内存(因为Tests析构时已经释放了容器内指针所指的内存),示例代码是为了演示问题而写的很直白,在实际的开发中,或许某个接口需要转入一个指针,而该指针又保存在某个指针容器中,那么此时就应该注意了,不小心就会写出如示例代码的错误。
通过release方法来获取元素的所有权
见如下代码,变量p就直接获取了Tests中第一个元素的所有权,那么Tests容器中就直接删除了第一个元素。
int main()
{
boost::ptr_vector<CTest> Tests;
Tests.push_back(new CTest(1));
Tests.push_back(new CTest(2));
boost::ptr_vector<CTest>::auto_type p = Tests.release(Tests.begin());
return 0;
}
auto_type
指针容器中有一个特殊的auto_type,boost文档中对其的描述如下
You can think of auto_type as a non-copyable form of std::auto_ptr
我们知道std::auto_ptr是通过复制,赋值来实现的转移语义,这里的auto_type是没有复制语义的,但它有移动语义,也是通过release方法。这跟std::unique_ptr很像也是只有移动语义,也只可通过release方法来转移所有权。见如下代码:
int main()
{
CTest *p1 = NULL;
{
boost::ptr_vector<CTest> Tests;
Tests.push_back(new CTest(1));
Tests.push_back(new CTest(2));
boost::ptr_vector<CTest>::auto_type p = Tests.release(Tests.begin());
p1 = p.release();
if (!p)
{
std::cout << "no ownership" << std::endl;
}
}
delete p1;
p1 = NULL;
//system("pause");
_CrtDumpMemoryLeaks();
return 0;
}
auto_type类型的变量p在将所有权转移给了p1后,如果不delete p1的话就会造成内存泄漏。
auto_type所有权的转移
上面提到auto_type类型是不可复制的,所以两个auto_type类型的变量,不能直接赋值来转移所有权,见如下代码:
int main()
{
boost::ptr_vector<CTest> Tests;
Tests.push_back(new CTest(1));
Tests.push_back(new CTest(2));
boost::ptr_vector<CTest>::auto_type p = Tests.pop_back();
boost::ptr_vector<CTest>::auto_type p1;
p1 = p;
return 0;
}
上面的代码是无法编译通过的,应该通过ptr_container提供的move函数来进行所有权转移
p1 = boost::ptr_container::move(p);
应用示例
在muduo日志库中,AsyncLogging类中通过交换内存实现了异步日志,在类中分为前端和一个后端线程,前端用于收集格式化后的日志,后端线程负责将收集的日志写入文件,前端和后端的数据交换是通过交换内存来实现的。这里不讨论其异步日志实现思想,主要是学习下其内存交换的实现思路。
在AsynLogging类中,前端和后端持有两个块buffer。buffer的定义如下:
//Buffer的类型
typedef muduo::detail::FixedBuffer<muduo::detail::kLargeBuffer> Buffer;
//指针容器用于存储多块Buffer
typedef boost::ptr_vector<Buffer> BufferVector;
//Buffer指针,声明为auto_type类型
typedef BufferVector::auto_type BufferPtr;
前端线程定义的Buffer指针成员
//前端当前使用的缓存
BufferPtr currentBuffer_;
//前端待用的缓存
BufferPtr nextBuffer_;
//用于前后端线程交换缓存的buffers
BufferVector buffers_;
前端缓存的初始化,前端持有currentBuffer_和nextBuffer_两块内存
AsyncLogging::AsyncLogging(const string& basename,
size_t rollSize,
int flushInterval)
: flushInterval_(flushInterval),
running_(false),
basename_(basename),
rollSize_(rollSize),
thread_(boost::bind(&AsyncLogging::threadFunc, this), "Logging"),
latch_(1),
mutex_(),
cond_(mutex_),
//初始化缓存
currentBuffer_(new Buffer),
//初始化缓存
nextBuffer_(new Buffer),
buffers_()
{
currentBuffer_->bzero();
nextBuffer_->bzero();
buffers_.reserve(16);
}
前端线程交换缓存
void AsyncLogging::append(const char* logline, int len)
{
muduo::MutexLockGuard lock(mutex_);
if (currentBuffer_->avail() > len)
{
currentBuffer_->append(logline, len);
}
else
{
//当正在使用的缓存空间不够用时,则使用nextBuffer_缓存
//将currentBuffer放入buffers_,准备作交换,注意这里currentBuffer_.release()即为放弃缓存所有权
buffers_.push_back(currentBuffer_.release());
if (nextBuffer_)
{
//将nextBuffer_的所有权转移给currentBuffer_(ptr_containts::auto_type不支持复制和赋值,只可通过move来转移所有权)
currentBuffer_ = boost::ptr_container::move(nextBuffer_);
}
else
{
currentBuffer_.reset(new Buffer); // Rarely happens
}
currentBuffer_->append(logline, len);
cond_.notify();
}
}
后端线程交换缓存,后端持有newBuffer1和newBuffer2两块内存
void AsyncLogging::threadFunc()
{
assert(running_ == true);
latch_.countDown();
LogFile output(basename_, rollSize_, false);
//后端线程所持有的两个缓存
BufferPtr newBuffer1(new Buffer);
BufferPtr newBuffer2(new Buffer);
newBuffer1->bzero();
newBuffer2->bzero();
//待写入文件的缓存
BufferVector buffersToWrite;
buffersToWrite.reserve(16);
while (running_)
{
assert(newBuffer1 && newBuffer1->length() == 0);
assert(newBuffer2 && newBuffer2->length() == 0);
assert(buffersToWrite.empty());
{
muduo::MutexLockGuard lock(mutex_);
if (buffers_.empty()) // unusual usage!
{
cond_.waitForSeconds(flushInterval_);
}
//后端线程被唤醒,后端线程获得lock锁
//交换前端正在使用的缓存
buffers_.push_back(currentBuffer_.release());
//交换给currentBuffer一个新的缓存
currentBuffer_ = boost::ptr_container::move(newBuffer1);
//将待写入文件的日志交换给buffersToWrite
buffersToWrite.swap(buffers_);
if (!nextBuffer_)
{
//给nextBuffer_一块新的缓存
nextBuffer_ = boost::ptr_container::move(newBuffer2);
}
}
assert(!buffersToWrite.empty());
if (buffersToWrite.size() > 25)
{//写文件
char buf[256];
snprintf(buf, sizeof buf, "Dropped log messages at %s, %zd larger buffers\n",
Timestamp::now().toFormattedString().c_str(),
buffersToWrite.size()-2);
fputs(buf, stderr);
output.append(buf, static_cast<int>(strlen(buf)));
buffersToWrite.erase(buffersToWrite.begin()+2, buffersToWrite.end());
}
for (size_t i = 0; i < buffersToWrite.size(); ++i)
{
// FIXME: use unbuffered stdio FILE ? or use ::writev ?
output.append(buffersToWrite[i].data(), buffersToWrite[i].length());
}
if (buffersToWrite.size() > 2)
{
// drop non-bzero-ed buffers, avoid trashing
//指针容器跟标准库容器一样在resize后会释放多余的成员
buffersToWrite.resize(2);
}
if (!newBuffer1)
{
assert(!buffersToWrite.empty());
newBuffer1 = buffersToWrite.pop_back();
newBuffer1->reset();
}
if (!newBuffer2)
{
assert(!buffersToWrite.empty());
newBuffer2 = buffersToWrite.pop_back();
newBuffer2->reset();
}
buffersToWrite.clear();
output.flush();
}
output.flush();
}
如上代码,通过指针容器实现内存的交换十分优雅,即不会有内存拷贝的memecpy操作,也不用专门去考虑内存释放问题,实现的代码也十分简洁易读。
相关资料:
http://www.boost.org/doc/libs/1_66_0/libs/ptr_container/doc/tutorial.html#indirected-interface
http://www.boost.org/doc/libs/1_66_0/libs/ptr_container/doc/examples.html
https://stackoverflow.com/questions/871267/how-do-you-transfer-ownership-of-an-element-of-boostptr-vector/871510#871510