开始正文之前,做一些背景铺垫,方便读者了解我的工程需求。我的项目是一个客户端消息分发中心,在连接上消息后台后,后台会不定时的给我推送一些消息,我再将它们转发给本机的其它桌面产品去做显示。后台为了保证消息一定可以推到客户端,它采取了一种重复推送的策略,也就是说,每次当我重新连接上后台时,后台会把一段时间内的消息都推给我、而不论这些消息之前是否已经推送过,如果我不加处理的直接推给产品,可能造成同一个消息重复展示多次的问题。为此,我在接收到消息后,会将它们保存在进程中的一个容器中,当有新消息到达时,会先在这个容器里检查有没有收到这条消息,如果有,就不再转发。
1namespace GCM { 2class server_msg_t 3 { 4public: 5voiddump(charconst* prompt); 6 7std::string appname; 8std::string uid; 9std::string msgid; 10time_t recv_first =0; 11time_t recv_last =0; 12intrecv_cnt =0; 13 };1415class WorkEngine16 {17public:18 WorkEngine();19~WorkEngine();2021private:22// to avoid server push duplicate messages to same client.23// note this instance is only accessed when single connection to server arrives message, so no lock needed..24std::vector m_svrmsgs;25 };26}
上面的是经过简化以后的代码,m_svrmsgs 成员存储的就是接收到的所有的后台消息,server_msg_t 代表的就是一个后台消息,appname、uid 用来定位发给哪个产品的哪个实例;msgid 用来唯一的标识一个消息;recv_first、recv_last、recv_cnt 分别表示消息接收的首次时间、最后时间以及重复接收次数。那么现在一个很现实的问题就是,我需要把这些消息序列化到永久存储上去,以便进程重启后这些信息还在。这里我使用了 sqlite 数据库,与此相关的代码封装在了 WorkEngine 的成员函数中,很容易想到的一种函数声明方式是这样:
1namespace GCM { 2class server_msg_t 3 { 4public: 5voiddump(charconst* prompt); 6 7std::string appname; 8std::string uid; 9std::string msgid; 10time_t recv_first =0; 11time_t recv_last =0; 12intrecv_cnt =0; 13 };1415class WorkEngine16 {17public:18 WorkEngine();19~WorkEngine();2021protected:22intdb_store_server_msg (std::vectorconst& vec); 23intdb_fetch_server_msg (std::vector & vec);2425private:26// to avoid server push duplicate messages to same client.27// note this instance is only accessed when single connection to server arrives message, so no lock needed..28std::vector m_svrmsgs;29 };30}31
像 line 22-23 展示的那样,直接使用 std::vector<server_msg_t> 这个容器作为参数(有的人可能觉得我多此一举,直接在函数里访问 m_svrmsgs 成员不就行了,为什么要通过参数传递呢?可能这个例子不太明显,但是确实存在一些情况容器是作为局部变量而非成员变量存在的,这里出于说明目的做了一些简化)。但是我觉得这样写太死板了,万一以后我换了容器呢,这里是不是还要改?也许是泛型算法看多了,总感觉这样写不够“通用”。但是如果写成下面这样,还是换汤不换药:
intdb_store_server_msg (std::vector::iterator beg, std::vector::iterator end);
参考标准库 std::copy 算法,将其改造一番,结果就成了这个样子:
template intdb_store_server_msg(InputIterator beg, InputIterator end);
叫成员函数模板,还是成员模板函数,还是模板成员函数……说不清楚,反正就是成员函数+模板函数。实现的话可以这样写:
1namespace GCM { 2template 3int WorkEngine::db_store_server_msg(InputIterator beg, InputIterator end) 4 { 5intret =0, rowid =0; 6 qtl::sqlite::database db(SQLITE_TIMEOUT); 7 8try 9 {10 db.open(get_db_path().c_str(), NULL);11writeInfoLog("open db for store server msg OK");1213 db.begin_transaction();1415for(auto it = beg; it != end; ++it)16 {17// 1th, insert or update user info18rowid = db.insert_direct("replace into server_msg (appname, uid, msgid, first_recv, last_recv, count) values (?, ?, ?, ?, ?, ?);", 19it->appname, it->uid, it->msgid, it->recv_first, it->recv_last, it->recv_cnt);2021ret++; 22 }2324 db.commit();25 db.close();26writeInfoLog("replace into %d records", ret); 27 }28catch(qtl::sqlite::error &e)29 {30writeInfoLog("manipute db for store server msg error: %s", e.what());31 db.rollback();32 db.close();33return-1;34 }3536return ret; 37 }38}
可以看到,核心代码就是对迭代器区间作遍历 (line 15)。调用方也是非常简洁:
db_store_server_msg(m_svrmsgs.begin(), m_svrmsgs.end());
一行搞定,看起来已经大功告成了,毫无难度可言,那么这篇文章想要说明什么呢?别着急,真正的难点在于从数据库恢复数据。首先直接使用迭代器是不行了,因为我们现在要往容器里插入元素,迭代器只能遍历元素,一点帮助也没有。但是相信读者一定看过类似这样的代码:
正在上传... 取消
1intmain (void) 2{ 3intarr[] = {1,3,5,7,11 }; 4 std::vector vec; 5std::copy (arr, arr +sizeof(arr) /sizeof(int), std::back_inserter(vec)); 6for(auto it = vec.begin (); it != vec.end (); ++ it) 7printf ("%d\n", *it); 8 9return0; 10}
正在上传... 取消
为了在容器尾部插入元素,标准库算法借助了 back_inserter 这个东东。于是自然而然的想到,我们这里能不能声明 back_inserter 作为输入参数呢? 例如像这样:
template intdb_fetch_server_msg(OutputIterator it);
模板实现这样写:
正在上传... 取消
1namespace GCM { 2template 3int WorkEngine::db_fetch_server_msg(OutputIterator it) 4 { 5intret =0; 6 qtl::sqlite::database db(SQLITE_TIMEOUT); 7 8try 9 {10 db.open(get_db_path().c_str(), NULL);11writeInfoLog("open db for fetch server msg OK");1213db.query("select appname, uid, msgid, first_recv, last_recv, count from server_msg", 14[&ret, &it](std::stringconst& appname, std::stringconst& uid, std::stringconst& msgid, time_t first_recv, time_t last_recv,int count) {15 server_msg_t sm; 16sm.appname = appname; 17sm.uid = uid; 18sm.msgid = msgid; 19sm.recv_first = first_recv; 20sm.recv_last = last_recv; 21sm.recv_cnt = count; 22*it = sm;23++ret; 24 }); 2526 db.close();27writeInfoLog("query %d records", ret);28 }29catch(qtl::sqlite::error &e)30 {31writeInfoLog("manipute db for store server msg error: %s", e.what());32 db.close();33return-1;34 }3536return ret;37 }38}
正在上传... 取消
其实核心就是一句对 back_inserter 的赋值语句 (line 22)。调用方同样是一行搞定:
db_fetch_server_msg (std::back_inserter(m_svrmsgs));
模板声明与模板实现的分离
上面的代码可以正常通过编译,但前提是模板实现与模板调用位于同一文件。考虑到这个类之前已经有许多逻辑,我决定将与数据库相关的内容,转移到一个新的文件(engine_db.cpp),来减少单个文件的代码量。调整后的文件结构如下:
+ engine.h: WorkEngine 声明
+ engine.cpp:WorkEngine 实现 (包含 engine.h)
+ engine_db.cpp:WorkEngine::db_xxx 模板实现 (包含 engine.h)
重新编译,报了一个链接错误:
1>workengine.obj : error LNK2001: 无法解析的外部符号 "protected: int __thiscall GCM::WorkEngine::db_fetch_server_msg<class std::back_insert_iterator<class std::vector<class GCM::server_msg_t,class std::allocator<class GCM::server_msg_t> > > >(class std::back_insert_iterator<class std::vector<class GCM::server_msg_t,class std::allocator<class GCM::server_msg_t> > >)" (??$db_fetch_server_msg@V?$back_insert_iterator@V?$vector@Vserver_msg_t@GCM@@V?$allocator@Vserver_msg_t@GCM@@@std@@@std@@@std@@@WorkEngine@GCM@@IAEHV?$back_insert_iterator@V?$vector@Vserver_msg_t@GCM@@V?$allocator@Vserver_msg_t@GCM@@@std@@@std@@@std@@@Z)
很明显是模板调用时找不到对应的链接所致。此时需要使用“模板显示实例化”在 engine_db.cpp 文件中强制模板生成对应的代码实体,来和 engine.cpp 中的调用点进行链接。需要在该文件开始处加入下面两行代码:
using namespace GCM;
templateintWorkEngine::db_fetch_server_msg > >(std::back_insert >);
注意模板成员函数显示实例化的语法,我专门查了下《cpp primer》,格式为:
template return_type CLASS::member_func (type1,type2, ……);
对应到上面的语句,就是使用 std::back_insert<std::vector<server_msg_t> > 代替原来的 OutputIterator 类型,来告诉编译器显示生成这样一个函数模板实例。注意这里相同的类型要写两遍,一遍是函数模板参数,一遍是函数参数。然而这个显示实例化语法却没有通过编译:
1>engine_db.cpp(15): error C2061: 语法错误: 标识符“back_inserter”
1>engine_db.cpp(15): error C2974: 'GCM::WorkEngine::db_fetch_server_msg' : 模板 对于 'OutputIterator'是无效参数,应为类型
1> f:\gdpclient\src\gcm\gcmsvc\workengine.h(137) : 参见“GCM::WorkEngine::db_fetch_server_msg”的声明
1>engine_db.cpp(15): error C3190: 具有所提供的模板参数的“int GCM::WorkEngine::db_fetch_server_msg(void)”不是“GCM::WorkEngine”的任何成员函数的显式实例化
1>engine_db.cpp(15): error C2945: 显式实例化不引用模板类专用化
百思不得其解。出去转了一圈,呼吸了一点新鲜空气,脑袋突然灵光乍现:之前不是有一长串的链接错误吗,把那个里面的类型直接拿来用,应该能通过编译!说干就干,于是有了下面这一长串显示实例化声明:
templateintGCM::WorkEngine::db_fetch_server_msg > > >(classstd::back_insert_iterator > >)
过分的是 —— 居然通过编译了!再仔细看看这一长串类型声明,貌似只是把 vector 展开了而已,我用“浓缩版”的 vector 再声明一次试下有什么变化:
templateintGCM::WorkEngine::db_fetch_server_msg > >(std::back_insert_iterator >);
居然也通过了。看来只是用 back_insert_iterator 代替了 back_inserter 就好了,back_insert_iterator 又是一个什么鬼?查看 back_inserter 定义,有如下发现:
1template inline back_insert_iterator<_Container>back_inserter(_Container& _Cont)2{// return a back_insert_iterator3return(_STDback_insert_iterator<_Container>(_Cont));4}
貌似 back_inserter 就是一个返回 back_insert_iterator 类型的模板函数,与 std::make_pair(a,b) 和 std::pair <A,B> 的关系很像,因为这里要的是一个类型,所以不能直接传 back_inserter 这个函数给显示实例化的声明。好,到目前我止,我们实现了用一个 inserter 或两个 iterator 参数代替笨拙的容器参数、并可以将声明、调用、实现分割在三个不同的文件中,已经非常完美。美中不足的是,模板显示实例化还有一些啰嗦,这里使用 typedef 定义要实例化的类型,将上面的语句改造的更清晰一些:
typedef std::back_insert_iterator > inserter_t;
template intWorkEngine::db_fetch_server_msg(inserter_t);
同理,对 db_store_server_msg 进行同样的改造:
typedef std::vector ::iterator iterator_t;
template intWorkEngine::db_store_server_msg(iterator_t, iterator_t);
这样是不是更完美了?
使用 map 代替 vector
在使用过程中,发现使用 map 可以更快更方便的查询消息是否已经在容器中,于是决定将消息容器定义变更如下:
std::map m_servmsgs;
其中 map 的 value 部分与之前不变,增加的 key 部分为 msgid。这样改了之后,遍历时要使用 "it->second." 代替 "it->";插入元素时需要使用 “*it = std::make_pair (sm.msgid, sm)” 代替 “*it = sm”。做完上述修改,我发现程序仍然编译不通过。经过一番排查,发现原来是 back_inserter 不能适配 map 容器。因为 back_inserter 对应的 back_insert_iterator 在 = 操作符中会调用容器的 push_back 接口,而这个接口仅有 vector、list、deque 几个容器支持,map 是不支持的。怎么办呢,幸好已经有好心人写好了 map 的插入器 —— map_inserter:
正在上传... 取消
1#pragmaonce 2 3namespace std 4{ 5template 6class map_inserter { 7 8public: 9typedef std::map<_Key, _Value, _Compare> map_type;10 typedef typename map_type::value_type value_type;1112private:13map_type &m_;1415public:16map_inserter(map_type &_m)17 : m_(_m)18 {}1920public:21template22class map_inserter_helper {23public:24typedef map_inserter<_K, _V, _Cmp> mi_type;25 typedef typename mi_type::map_type map_type;26 typedef typename mi_type::value_type value_type;2728map_inserter_helper(map_type &_m)29 :m_(_m)30 {}3132constvalue_type &operator= (constvalue_type & v) {33m_[v.first] = v.second;34return v;35 }36private:37map_type&m_;38 };3940typedef map_inserter_helper<_Key, _Value, _Compare> mi_helper_type;41mi_helper_typeoperator* () {42return mi_helper_type(m_);43 }4445map_inserter<_Key, _Value, _Compare> &operator++() {46return*this;47 }4849map_inserter<_Key, _Value, _Compare> &operator++(int) {50return*this;51 }5253 };5455template56map_inserter<_K, _V, _Cmp> map_insert(std::map<_K, _V, _Cmp> &m) {57returnmap_inserter<_K, _V, _Cmp>(m);58 }59};
正在上传... 取消
这段代码我是从网上抄来的,具体请参考下面的链接:std::map 的 inserter 实现。然而不幸的是,这段代码“残疾”了,不知道是作者盗链、还是没有输入完整的原因,这段代码有一些先天语法缺失,导致它甚至不能通过编译,在我的不懈“脑补”过程下,缺失的部分已经通过高亮部位补齐了,众位客官可以直接享用~
特别需要说明的是,最有技术含量的缺失发生在 line 37 的一个引用符,如果没有加入这个,虽然可以通过编译,但在运行过程中,inserter 不能向 map 中插入元素,会导致从数据库读取完成后得到空的 map。我一直尝试查找这个文章的原文,但是一无所获,对于互联网传播过程中发现这样驴头马嘴的讹误事件,本人表示非常痛心疾首(虽然我不是很懂,但你也不能坑我啊)……
好了,话归正题,有了 map_inserter 后,我们就可以这样声明了:
typedef std::map_inserter > inserter_t;
template intWorkEngine::db_fetch_server_msg(inserter_t);
对于这个 map_inserter 实现,我们需要传递 map 的三个模板参数,而不是 map 本身这个参数,我不太清楚是一种进步、还是一种退步,反正这个 map_inserter 有点儿怪,没有封装成 map_insert_iterator + map_inserter 的形式,和标准库的实现水平还是有差异的,大家将就看吧。调用方也需要进行一些微调:
db_fetch_server_msg(std::map_inserter >(m_svrmsgs));
看看,没有标准库实现的简洁吧,到底是山寨货啊~ 幸好我们已经封装了 inserter_t 类型,可以改写成这样:
db_fetch_server_msg(inserter_t(m_svrmsgs));
简洁多了。现在我们再看下项目的文件组成:
1
2
3
4
5
+ map_inserter.hpp: map_inserter 声明+实现
+ engine.h: WorkEngine 声明 (包含 map_inserter.hpp)
+ engine.cpp:WorkEngine 实现 (包含 engine.h)
+ engine_db.cpp:WorkEngine::db_xxx 模板实现 (包含 engine.h)
……
这里为了降低复杂度,将 map_inserter 放在头文件中进行共享,类似于标准库头文件的使用方式。
使用普通模板函数代替类成员模板函数
本文的最后,我们再回头看一下上面例子中的两个成员模板函数,发现它们并没有使用到类中的其它成员,其实完全可以将它们独立成两个普通模板函数去调用,例如改成这样:
正在上传... 取消
1namespaceGCM {2classserver_msg_t3{4public:5void dump(charconst*prompt);67 std::stringappname;8 std::stringuid;9 std::stringmsgid;10 time_t recv_first =0;11 time_t recv_last =0;12int recv_cnt =0;13};1415classWorkEngine16{17public:18WorkEngine();19 ~WorkEngine();2021private:22//to avoid server push duplicate messages to same client.23//note this instance is only accessed when single connection to server arrives message, so no lock needed..24 std::vectorm_svrmsgs;25};2627 template 28intdb_store_server_msg(InputIterator beg, InputIterator end);29 template 30intdb_fetch_server_msg(OutputIterator it);3132 typedef std::map ::iterator iterator_t;33 typedef std::map_inserter >inserter_t;34 }
正在上传... 取消
将模板函数声明从类中移到类外(line 27-30),同时修改 engine_db.cpp 中两个类的定义和显示实例化语句,去掉类限制(WorkEngine::):
templateintdb_fetch_server_msg(inserter_t);
template intdb_store_server_msg(iterator_t, iterator_t);
调用处不需要修改。再次编译报错:
1>engine_db.cpp(16): warning C4667: “int GCM::db_fetch_server_msg(GCM::inserter_t)”: 未定义与强制实例化匹配的函数模板
1>engine_db.cpp(17): warning C4667: “int GCM::db_store_server_msg(GCM::iterator_t,GCM::iterator_t)”: 未定义与强制实例化匹配的函数模板
1> 正在创建库 F:\gdpclient\src\gcm\Release\gcmsvc.lib 和对象 F:\gdpclient\src\gcm\Release\gcmsvc.exp
1>workengine.obj : error LNK2001: 无法解析的外部符号 "int __cdecl GCM::db_fetch_server_msg<class std::map_inserter<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >,class GCM::server_msg_t,struct std::less<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > > > >(class std::map_inserter<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >,class GCM::server_msg_t,struct std::less<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > > >)" (??$db_fetch_server_msg@V?$map_inserter@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@Vserver_msg_t@GCM@@U?$less@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@2@@std@@@GCM@@YAHV?$map_inserter@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@Vserver_msg_t@GCM@@U?$less@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@2@@std@@@Z)
1>workengine.obj : error LNK2001: 无法解析的外部符号 "int __cdecl GCM::db_store_server_msg<class std::_Tree_iterator<class std::_Tree_val<struct std::_Tree_simple_types<struct std::pair<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const ,class GCM::server_msg_t> > > > >(class std::_Tree_iterator<class std::_Tree_val<struct std::_Tree_simple_types<struct std::pair<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const ,class GCM::server_msg_t> > > >,class std::_Tree_iterator<class std::_Tree_val<struct std::_Tree_simple_types<struct std::pair<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const ,class GCM::server_msg_t> > > >)" (??$db_store_server_msg@V?$_Tree_iterator@V?$_Tree_val@U?$_Tree_simple_types@U?$pair@$$CBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@Vserver_msg_t@GCM@@@std@@@std@@@std@@@std@@@GCM@@YAHV?$_Tree_iterator@V?$_Tree_val@U?$_Tree_simple_types@U?$pair@$$CBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@Vserver_msg_t@GCM@@@std@@@std@@@std@@@std@@0@Z)
前两个 warning 是因为由成员函数变为普通函数后,显示实例化需要放在函数实现后面,我们将这两条语句调整到文件末尾就好了。对于后面两个链接 error,百思不得其解,后来使用一个非常简单的 test 模板函数做试验,发现是命名空间搞的鬼,需要在每个函数的定义和显示实例化语句前加上命名空间限定(GCM::):
templateintGCM::db_fetch_server_msg(inserter_t);
template intGCM::db_store_server_msg(iterator_t, iterator_t);
亚马逊测评 www.yisuping.com