使用 c++ 模板顯示實例化解決模板函數聲明與實現分離的問題
問題背景
開始正文之前,做一些背景鋪墊,方便讀者了解我的工程需求。我的項目是一個客戶端消息分發中心,在連接上消息後台後,後台會不定時的給我推送一些消息,我再將它們轉發給本機的其它桌面產品去做顯示。後台為了保證消息一定可以推到客戶端,它採取了一種重複推送的策略,也就是說,每次當我重新連接上後台時,後台會把一段時間內的消息都推給我、而不論這些消息之前是否已經推送過,如果我不加處理的直接推給產品,可能造成同一個消息重複展示多次的問題。為此,我在接收到消息後,會將它們保存在進程中的一個容器中,當有新消息到達時,會先在這個容器里檢查有沒有收到這條消息,如果有,就不再轉發。
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表達式
[4]. C++函數模板的編譯方式
[5]. c++函數模板聲明與定義相分離
[6]. C++模板之函數模板實例化和具體化
[7]. C++ 函數模板 實例化和具體化
[8]. C++模板之隱式實例化、顯示實例化、隱式調用、顯示調用和模板特化詳解
[9]. c++模板函數聲明和定義分離
[10]. C++模板編程:如何使非通用的模板函數實現聲明和定義分離