一、容器存储
在前面的文章中一个重要的推荐应用就是在可能的情况下尽量使用STL中的标准容器,这样除了安全以后,最重要的是通用性较高。但在实际的应用中,有很多场景下需要在容器中存储大量的目标对象,比如在高并发的网络下,服务端需要存储客户端的一些相关对象。而在这些应用中,这些目标对象有大有小,有平凡类型也有复杂的类型,甚至有可能会提出多态的需求。那么,问题就产生了,在向容器中存储目标时,是存储对象目标还是存储指针目标呢?
本文就针对这种实际工程中的应用开发进行分析和说明,从而能够给大家一个可以借鉴的方法和原则。
二、存储目标
刚刚提到,容器中既可以存储对象也可以存储指针,那么这两种情况下会有什么问题呢?
- 对象的存储
直接存储对象简单方便,但对于复杂类型,特别是存在内部的指针数据或者需要多态处理的情况下,就无法达到开发者的要求了。同样,如果对象的目标较大,在数据的数量达到一程度后,存储的开销也是一个问题。还有,对象生命周期也要限制与容器相匹配的值语义需求 - 指针的存储
存储指针,容器中的存储大小是可控线性增长的(N*指针大小),一般上来讲,很难出现容器应付不足(因为对象可能先分配不出来了)。但使用指针最主要的是使得整体的操作变得复杂起来,开发者需要进行内存的主动回收。如果控制不好,就可以会出现多次释放内存或使用已经释放内存的情况。最后,使用指针则无法保证对象的生命周期与容器的生命周期相匹配(即双方生命周期互相独立)
在上面的两个问题基础上,又可以展开下面的问题:
- 内部指针的问题
如果存储的对象内部存在指针类型数据怎么办?甚至更复杂的情况会有反复嵌套的情况。这就又引出一个经典的例子,深浅拷贝。如果此时存储对象,以vector为例,它会在push_back时,做拷贝的动作。如果类中未进行深拷贝的处理,很容易出现内部指针数据悬垂的现象,这是极其危险的。 - 多态处理
而如果存储的类对象是存在多态行为的,而在实际的应用中,又希望能够通过容器的存储来动态的支持不同父子对象的行为处理。那么,存储指针是一种强选择了。 - 移动语义
在C++11以后,提供了移动语义,那么在一些较大甚至大型的对象面前,是否可以使用移动语义来实现对象的存储呢?目前看在有些场景下是可以满足需求的。
三、处理方法和原则
看到上面的分析,其实就可以通过下面的方法来处理存储的类型:
- 存储对象
一般上来讲,如果是平凡类型或原来的POD类型,也就是说,这个对象只是基础类型的数据描述,而且对象的内存空间不是多大(到底什么是大和小,需要开发者根据实际场景来自己确定),容器存储尽量选择以对象的方式来存储。另外一些类对象可能相对简单,对象也不大,应用也没有多态等的要求,此时也可以考虑使用对象直接存储到容器中。
这样操作起来直观、简单,也更容易维护。 - 存储指针(智能指针)
如果容器存储的对象很大,此时一般是考虑存储指针。但存储指针就比有刚刚提到的内存操作风险问题,所以非特殊场景下(比如和C库的接口交互),不推荐使用普通指针,而是使用智能指针。如果一定要使用普通指针,请使用RAII等技术保证内存资源的安全 - 多态等需求
在需要应用多态的情况下,原则上推荐使用智能指针,这样既能够满足多态的需要又可以满足内存安全操作的需求 - 支持移动语义,可以考虑使用对象
如果开发者已经在C++11以后开发,在自己希望使用大对象时,可考虑使用移动语义来满足在容器中存储对象的要求。但尽量要满足其它的限制条件,不能因为可以而认为一定可以 - 内部有指针数据
当类中存在指针数据,即类中嵌套有对象指针。此时有两种情况,如果使用的是智能指针,则可以等同于对"存储指针"的处理。但如果存储的是普通指针,则需要进行深拷贝的处理(编写拷贝函数) - 存储对象需要共享
如果容器中存储的目标需要为多个场景共享,此时一般是推荐使用智能指针。这样可以极大的减少拷贝或内存安全问题
通过上面的分析,整体的把控原则就是:考虑拷贝的开销控制并兼顾移动语义来满足性能需求;支持多态和共享来达到应用目的;保证内存资源的安全管理和是否对缓存友好。
四、例程
下面看一个工程中实践的代码:
c
#ifndef DATAPARSE_MEMCACHEEX_H
#define DATAPARSE_MEMCACHEEX_H
#include <string>
#include <unordered_map>
#include <memory>
#include <vector>
#include <queue>
#include <mutex>
#include <assert.h>
#include "MemCache.h"
namespace dataparse
{
template<typename T,typename U,typename E>
class MemCacheEx
{
public:
MemCacheEx() {}
~MemCacheEx() {}
public:
int InitMemCache(int len = global::CACHE_NUM)
{
int num = this->CreateMemCache(len);
return num;
}
std::shared_ptr<U> QueryAskMap(const T &id, const T &pbkey)
{
auto au = this->umemAsk_.find(id);
if (au != this->umemAsk_.end())
{
return au->second;
}
return nullptr;
}
std::shared_ptr<U> QueryAckMap(const T &id, const T &pbkey)
{
auto au = this->umemAck_.find(id);
if (au != this->umemAck_.end())
{
return au->second;
}
return nullptr;
}
//此处的ID为MD5值,不再是USERID,其它同样
int AddMsgToMemCache(const T &id, const T & userid, char *buf, int len, CacheType type)
{
//缓存单元赋值
//增加对长度的判断,如果超过10万条就删除索引在前面的一条
std::shared_ptr<U> pd = nullptr;
bool exist = true;
//已经存在
if (CacheType::ASK == type)
{
pd = this->QueryAskMap(id,"");
}
else
{
pd = this->QueryAckMap(id,"");
}
//全新存储
if (nullptr == pd)
{
exist = false;
pd = this->GetMsgCacheDataFromQueue();
pd->md_ = id;
pd->userid_ = userid;
pd->index_ = ++this->count_;
}
E cd;
cd.len = len;
memmove(cd.buf, buf, len);
pd->vec_.emplace_back(cd);
if (exist)
{
return 1;
}
//存储MAP
if (CacheType::ASK == type)
{
this->umemAsk_.emplace(std::pair<T, std::shared_ptr<U>>(id, pd));
}
else
{
this->umemAck_.emplace(std::pair<T, std::shared_ptr<U>>(id, pd));
}
return 0;
}
int DeleteMsgFromMemCache(const T & id, CacheType type)
{
//删除
std::unordered_map<T, std::shared_ptr<U>> * tmp = nullptr;
if (type == CacheType::ASK)
{
tmp = &this->umemAsk_;
}
else
{
tmp = &this->umemAck_;
}
auto search = tmp->find(id);
if (search != tmp->end())
{
this->SetMsgCacheDataToQueue(search->second);
//删除
tmp->erase(search);
return 1;
}
else
{
return 0;
}
}
int GetMemCacheSize(CacheType ct)
{
if (ct == CacheType::ACK)
{
return this->umemAck_.size();
}
else if(ct == CacheType::ASK)
{
return this->umemAsk_.size();
}
return 0;
}
//聊天转发缓存
std::shared_ptr<U> GetMemFromCache()
{
std::shared_ptr<U> tmp = nullptr;
if(this->queue_.size() > 0)
{
std::lock_guard<std::mutex> guard(this->mutex_);
tmp = this->queue_.front();
this->queue_.pop();
}
else
{
std::lock_guard<std::mutex> guard(this->mutex_);
tmp = std::make_shared<U>();
}
assert(tmp);
return tmp;
}
void SetMemToCache(std::shared_ptr<U> p)
{
//初始化
p->len = 0;
p->type = 0;
p->ptmp = nullptr;
p->pfind = nullptr;
this->queue_.emplace(p);
}
private:
int CreateMemCache(int len)
{
for (int num = 0; num < len; num++)
{
std::shared_ptr<U> pCache = std::make_shared<U>();
this->queue_.emplace(pCache);
}
return len;
}
std::shared_ptr<U> GetMsgCacheDataFromQueue()
{
std::lock_guard<std::mutex> lock(this->mutex_);
if (this->queue_.size() < 5)
{
this->CreateMemCache(100);
}
std::shared_ptr<U> pd = this->queue_.front();
this->queue_.pop();
return pd;
}
int SetMsgCacheDataToQueue(std::shared_ptr<U> pd)
{
//初始化可以用相关数据对象的本身来处理
std::lock_guard<std::mutex> lock(this->mutex_);
pd->index_ = 0;
pd->md_ = "";
pd->userid_ = "";
pd->vec_.clear();
this->queue_.emplace(pd);
return 0;
}
private:
//请求消息队列和返回结果消息队列
std::unordered_map<T, std::shared_ptr<U>> umemAsk_;
std::unordered_map<T, std::shared_ptr<U>> umemAck_;
std::queue<std::shared_ptr<U>> queue_;
std::mutex mutex_;
int count_ = 0; //index的计数
};
}
#endif // !DATAPARSE_MEMCACHEEX_H
代码非常简单,不再做分析说明。
五、总结
教材上的知识讲得很多,很细。但一个重要的问题在于,如何把理论的技术知识和实践紧密的结合起来。如果一个一个的来试,肯定会遇到不少的坑。甚至有些技术点可能都无法简单的通过应用来验证。这就需要开发者能够解决如何把抽象的知识和实践应用通过某种巧妙的设计整合在一起,然后进行互相的反馈,从而能够迅速的将二者结合以达到真正理解并掌握的目的。
至于这个巧妙的设计从何而来?就需要不断的思考并学习别人的开发经验。特别是网上不错的开源代码。