使用 c++ 模板显示实例化解决模板函数声明与实现分离的问题

时间:2024-01-27 17:01:44

问题背景

开始正文之前,做一些背景铺垫,方便读者了解我的工程需求。我的项目是一个客户端消息分发中心,在连接上消息后台后,后台会不定时的给我推送一些消息,我再将它们转发给本机的其它桌面产品去做显示。后台为了保证消息一定可以推到客户端,它采取了一种重复推送的策略,也就是说,每次当我重新连接上后台时,后台会把一段时间内的消息都推给我、而不论这些消息之前是否已经推送过,如果我不加处理的直接推给产品,可能造成同一个消息重复展示多次的问题。为此,我在接收到消息后,会将它们保存在进程中的一个容器中,当有新消息到达时,会先在这个容器里检查有没有收到这条消息,如果有,就不再转发。

 1 namespace GCM {
 2     class server_msg_t 
 3     {
 4     public:
 5         void dump(char const* prompt); 
 6     
 7         std::string appname; 
 8         std::string uid; 
 9         std::string msgid; 
10         time_t recv_first = 0; 
11         time_t recv_last = 0; 
12         int recv_cnt = 0; 
13     };
14     
15     class WorkEngine
16     {
17     public:
18         WorkEngine();
19         ~WorkEngine();
20     
21     private:
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::vector<server_msg_t> m_svrmsgs;
25     };
26 }

 

上面的是经过简化以后的代码,m_svrmsgs 成员存储的就是接收到的所有的后台消息,server_msg_t 代表的就是一个后台消息,appname、uid 用来定位发给哪个产品的哪个实例;msgid 用来唯一的标识一个消息;recv_first、recv_last、recv_cnt 分别表示消息接收的首次时间、最后时间以及重复接收次数。那么现在一个很现实的问题就是,我需要把这些消息序列化到永久存储上去,以便进程重启后这些信息还在。这里我使用了 sqlite 数据库,与此相关的代码封装在了 WorkEngine 的成员函数中,很容易想到的一种函数声明方式是这样:

 1 namespace GCM {
 2     class server_msg_t 
 3     {
 4     public:
 5         void dump(char const* prompt); 
 6     
 7         std::string appname; 
 8         std::string uid; 
 9         std::string msgid; 
10         time_t recv_first = 0; 
11         time_t recv_last = 0; 
12         int recv_cnt = 0; 
13     };
14     
15     class WorkEngine
16     {
17     public:
18         WorkEngine();
19         ~WorkEngine();
20     
21     protected:
22         int db_store_server_msg (std::vector<server_msg_t> const& vec); 
23         int db_fetch_server_msg (std::vector<server_msg_t> & vec);
24     
25     private:
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..
28         std::vector<server_msg_t> m_svrmsgs;
29     };
30 }
31         

 

像 line 22-23 展示的那样,直接使用 std::vector<server_msg_t> 这个容器作为参数(有的人可能觉得我多此一举,直接在函数里访问 m_svrmsgs 成员不就行了,为什么要通过参数传递呢?可能这个例子不太明显,但是确实存在一些情况容器是作为局部变量而非成员变量存在的,这里出于说明目的做了一些简化)。但是我觉得这样写太死板了,万一以后我换了容器呢,这里是不是还要改?也许是泛型算法看多了,总感觉这样写不够“通用”。但是如果写成下面这样,还是换汤不换药:

int db_store_server_msg (std::vector<server_msg_t>::iterator beg, std::vector<server_msg_t>::iterator end); 

 

参考标准库 std::copy 算法,将其改造一番,结果就成了这个样子:

template <class InputIterator>
int db_store_server_msg(InputIterator beg, InputIterator end); 

 

叫成员函数模板,还是成员模板函数,还是模板成员函数……说不清楚,反正就是成员函数+模板函数。实现的话可以这样写:

 1 namespace GCM {
 2     template <class InputIterator>
 3     int WorkEngine::db_store_server_msg(InputIterator beg, InputIterator end)
 4     {
 5         int ret = 0, rowid = 0; 
 6         qtl::sqlite::database db(SQLITE_TIMEOUT);
 7     
 8         try
 9         {
10             db.open(get_db_path().c_str(), NULL);
11             writeInfoLog("open db for store server msg OK");
12     
13             db.begin_transaction();
14     
15             for (auto it = beg; it != end; ++it)
16             {
17                 // 1th, insert or update user info
18                 rowid = db.insert_direct("replace into server_msg (appname, uid, msgid, first_recv, last_recv, count) values (?, ?, ?, ?, ?, ?);", 
19                     it->appname, it->uid, it->msgid, it->recv_first, it->recv_last, it->recv_cnt);
20     
21                 ret++; 
22             }
23     
24             db.commit();
25             db.close();
26             writeInfoLog("replace into %d records", ret); 
27         }
28         catch (qtl::sqlite::error &e)
29         {
30             writeInfoLog("manipute db for store server msg error: %s", e.what());
31             db.rollback();
32             db.close();
33             return -1;
34         }
35     
36         return ret; 
37     }
38 }

 

可以看到,核心代码就是对迭代器区间作遍历 (line 15)。调用方也是非常简洁:

db_store_server_msg(m_svrmsgs.begin(), m_svrmsgs.end()); 

 

一行搞定,看起来已经大功告成了,毫无难度可言,那么这篇文章想要说明什么呢?别着急,真正的难点在于从数据库恢复数据。首先直接使用迭代器是不行了,因为我们现在要往容器里插入元素,迭代器只能遍历元素,一点帮助也没有。但是相信读者一定看过类似这样的代码:

 1 int main (void)
 2 {
 3     int arr[] = { 1, 3, 5, 7, 11 }; 
 4     std::vector vec; 
 5     std::copy (arr, arr + sizeof (arr) / sizeof (int), std::back_inserter(vec)); 
 6     for (auto it = vec.begin (); it != vec.end (); ++ it) 
 7         printf ("%d\n", *it); 
 8 
 9     return 0; 
10 }

 

为了在容器尾部插入元素,标准库算法借助了 back_inserter 这个东东。于是自然而然的想到,我们这里能不能声明 back_inserter 作为输入参数呢? 例如像这样:

template <class OutputIterator>
int db_fetch_server_msg(OutputIterator it);

 

模板实现这样写:

 1 namespace GCM {
 2     template <class OutputIterator>
 3     int WorkEngine::db_fetch_server_msg(OutputIterator it)
 4     {
 5         int ret = 0;
 6         qtl::sqlite::database db(SQLITE_TIMEOUT);
 7     
 8         try
 9         {
10             db.open(get_db_path().c_str(), NULL);
11             writeInfoLog("open db for fetch server msg OK");
12     
13             db.query("select appname, uid, msgid, first_recv, last_recv, count from server_msg", 
14                 [&ret, &it](std::string const& appname, std::string const& uid, std::string const& msgid, time_t first_recv, time_t last_recv, int count) {
15                     server_msg_t sm; 
16                     sm.appname = appname; 
17                     sm.uid = uid; 
18                     sm.msgid = msgid; 
19                     sm.recv_first = first_recv; 
20                     sm.recv_last = last_recv; 
21                     sm.recv_cnt = count; 
22                     *it = sm; 
23                     ++ret; 
24             }); 
25     
26             db.close();
27             writeInfoLog("query %d records", ret);
28         }
29         catch (qtl::sqlite::error &e)
30         {
31             writeInfoLog("manipute db for store server msg error: %s", e.what());
32             db.close();
33             return -1;
34         }
35     
36         return 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;
template int WorkEngine::db_fetch_server_msg<std::back_insert<std::vector<server_msg_t> > >(std::back_insert<std::vector<server_msg_t> >);

 

注意模板成员函数显示实例化的语法,我专门查了下《cpp primer》,格式为:

template return_type CLASS::member_func<type1, type2, ……> (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: 显式实例化不引用模板类专用化

 

百思不得其解。出去转了一圈,呼吸了一点新鲜空气,脑袋突然灵光乍现:之前不是有一长串的链接错误吗,把那个里面的类型直接拿来用,应该能通过编译!说干就干,于是有了下面这一长串显示实例化声明:

template int 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> > >)

 

过分的是 —— 居然通过编译了!再仔细看看这一长串类型声明,貌似只是把 vector 展开了而已,我用“浓缩版”的 vector 再声明一次试下有什么变化:

template int GCM::WorkEngine::db_fetch_server_msg<std::back_insert_iterator<std::vector<server_msg_t> > >(std::back_insert_iterator<std::vector<server_msg_t> >);

 

 居然也通过了。看来只是用 back_insert_iterator 代替了 back_inserter 就好了,back_insert_iterator 又是一个什么鬼?查看 back_inserter 定义,有如下发现:

1 template<class _Container> inline back_insert_iterator<_Container> back_inserter(_Container& _Cont)
2 {    // return a back_insert_iterator
3     return (_STD back_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<std::vector <server_msg_t> > inserter_t;
template int WorkEngine::db_fetch_server_msg<inserter_t>(inserter_t);

 

同理,对 db_store_server_msg 进行同样的改造:

typedef std::vector <std::string, server_msg_t>::iterator iterator_t;
template int WorkEngine::db_store_server_msg<iterator_t>(iterator_t, iterator_t);

 

这样是不是更完美了?

使用 map 代替 vector

在使用过程中,发现使用 map 可以更快更方便的查询消息是否已经在容器中,于是决定将消息容器定义变更如下:

std::map<std::string, server_msg_t> 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 #pragma once
 2 
 3 namespace std
 4 {
 5     template <class _Key, class _Value, class _Compare>
 6     class map_inserter {
 7 
 8     public:
 9         typedef std::map<_Key, _Value, _Compare> map_type;
10         typedef typename map_type::value_type value_type;
11 
12     private:
13         map_type &m_;
14 
15     public:
16         map_inserter(map_type &_m)
17             : m_(_m)
18         {}
19 
20     public:
21         template <class _K, class _V, class _Cmp>
22         class map_inserter_helper {
23         public:
24             typedef 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;
27 
28             map_inserter_helper(map_type &_m)
29                 :m_(_m)
30             {}
31 
32             const value_type & operator= (const value_type & v) {
33                 m_[v.first] = v.second;
34                 return v;
35             }
36         private:
37             map_type &m_;
38         };
39 
40         typedef map_inserter_helper<_Key, _Value, _Compare> mi_helper_type;
41         mi_helper_type operator* () {
42             return mi_helper_type(m_);
43         }
44 
45         map_inserter<_Key, _Value, _Compare> &operator++() {
46             return *this;
47         }
48 
49         map_inserter<_Key, _Value, _Compare> &operator++(int) {
50             return *this;
51         }
52 
53     };
54 
55     template <class _K, class _V, class _Cmp>
56     map_inserter<_K, _V, _Cmp> map_insert(std::map<_K, _V, _Cmp> &m) {
57         return map_inserter<_K, _V, _Cmp>(m);
58     }
59 }; 

 

这段代码我是从网上抄来的,具体请参考下面的链接:std::map 的 inserter 实现。然而不幸的是,这段代码“残疾”了,不知道是作者盗链、还是没有输入完整的原因,这段代码有一些先天语法缺失,导致它甚至不能通过编译,在我的不懈“脑补”过程下,缺失的部分已经通过高亮部位补齐了,众位客官可以直接享用~

特别需要说明的是,最有技术含量的缺失发生在 line 37 的一个引用符,如果没有加入这个,虽然可以通过编译,但在运行过程中,inserter 不能向 map 中插入元素,会导致从数据库读取完成后得到空的 map。我一直尝试查找这个文章的原文,但是一无所获,对于互联网传播过程中发现这样驴头马嘴的讹误事件,本人表示非常痛心疾首(虽然我不是很懂,但你也不能坑我啊)……

好了,话归正题,有了 map_inserter 后,我们就可以这样声明了:

typedef std::map_inserter<std::string, server_msg_t, std::less<std::string> > inserter_t;
template int WorkEngine::db_fetch_server_msg<inserter_t>(inserter_t);

 

对于这个 map_inserter 实现,我们需要传递 map 的三个模板参数,而不是 map 本身这个参数,我不太清楚是一种进步、还是一种退步,反正这个 map_inserter 有点儿怪,没有封装成 map_insert_iterator + map_inserter 的形式,和标准库的实现水平还是有差异的,大家将就看吧。调用方也需要进行一些微调:

db_fetch_server_msg(std::map_inserter<std::string, server_msg_t, std::less <std::string> >(m_svrmsgs));

 

看看,没有标准库实现的简洁吧,到底是山寨货啊~ 幸好我们已经封装了 inserter_t 类型,可以改写成这样:

db_fetch_server_msg(inserter_t(m_svrmsgs));

 

简洁多了。现在我们再看下项目的文件组成:

+ 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 放在头文件中进行共享,类似于标准库头文件的使用方式。

使用普通模板函数代替类成员模板函数

本文的最后,我们再回头看一下上面例子中的两个成员模板函数,发现它们并没有使用到类中的其它成员,其实完全可以将它们独立成两个普通模板函数去调用,例如改成这样:

 1 namespace GCM {
 2     class server_msg_t 
 3     {
 4     public:
 5         void dump(char const* prompt); 
 6     
 7         std::string appname; 
 8         std::string uid; 
 9         std::string msgid; 
10         time_t recv_first = 0; 
11         time_t recv_last = 0; 
12         int recv_cnt = 0; 
13     };
14     
15     class WorkEngine
16     {
17     public:
18         WorkEngine();
19         ~WorkEngine();
20         
21     private:
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::vector<server_msg_t> m_svrmsgs;
25     };
26 
27     template <class InputIterator>
28     int db_store_server_msg(InputIterator beg, InputIterator end); 
29     template <class OutputIterator>
30     int db_fetch_server_msg(OutputIterator it);
31     
32     typedef std::map <std::string, server_msg_t>::iterator iterator_t;
33     typedef std::map_inserter<std::string, server_msg_t, std::less<std::string> > inserter_t;
34 }

 

将模板函数声明从类中移到类外(line 27-30),同时修改 engine_db.cpp 中两个类的定义和显示实例化语句,去掉类限制(WorkEngine::):

template int db_fetch_server_msg<inserter_t>(inserter_t);
template int db_store_server_msg<iterator_t>(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::):

template int GCM::db_fetch_server_msg<inserter_t>(inserter_t);
template int GCM::db_store_server_msg<iterator_t>(iterator_t, iterator_t);

 

可以看到,类成员模板函数和普通模板函数差别还是蛮大的,因为类本身也是一种命名空间,它的出现简化了其中成员函数的寻址。

结语

其实本文讲解了一种通用的通过 iterator 读取容器、通过 inserter 插入容器元素的方法,这种方式较之直接传递容器本身“优雅”不少,虽然不能实现 100% 无缝切换容器,但是也提供了极大的灵活性。特别是还研究了如何将这种方式实现的模板函数在不同文件中分别声明与实现,达到解除代码耦合的目的,具有较强的实用性。当然,这里仅仅是使用了模板实例化的方式,如果遇到模板不同的 TYPE 需要使用不同的函数实现的话,你可能还要遭遇模板特化语法(包括全特化与偏特化),那样复杂度还会上升,这里没有做进一步探索。

参考

[1]. C++ 11 Lambda表达式

[2]. std::map 的 inserter 实现

[3]. C++ 模板类的声明与实现分离问题(模板实例化)

[4]. C++函数模板的编译方式

[5]. c++函数模板声明与定义相分离

[6]. C++模板之函数模板实例化和具体化

[7]. C++ 函数模板 实例化和具体化

[8]. C++模板之隐式实例化、显示实例化、隐式调用、显示调用和模板特化详解

[9]. c++模板函数声明和定义分离

[10]. C++模板编程:如何使非通用的模板函数实现声明和定义分离