《Effective C++ 中文版 第三版》讀書筆記
條款 43:學習處理模板化基類內的名稱
我們需要一個程序,傳送信息到不同的公司去。信息要不譯成密碼,要不就是未加工的文字。如果編譯期間我們有足夠信息來決定哪一個信息傳至那一家公司,就可以采用基于 template 的解法:
class CompanyA{
public:
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
};
class CompanyB{
public:
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
};
... //針對其他公司設計的classes
class MsgInfo{...}; //這個class用來保存信息,以備將來產生信息
template<typename Company>
class MsgSender{
public:
void sendClear(const MsgInfo& info)
{
std::string msg;
根據info產生信息;
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info)
{
...;//調用c.sendEncrypted,類似sendClear
}
};
這個做法行的通。但假設我們有時候想要在每次發送出信息的時候志記(log)某些信息。 derived class 可以輕易加上這樣的行為,那似乎是個合情理的解法:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
void sendClearMsg(const MsgInfo& info)
{
將傳送前信息寫至log;
sendClear(info);
}
};
sendClearMsg 避免遮掩 “繼承而得的名稱”(條款 33),避免重新定義一個繼承而得的 non-virtual 函數(條款 36)。但上述代碼無法通過編譯,編譯器看不到 sendClear。為什么?
問題在于,編譯器遇到 class template LoggingMsgSender 定義式時,并不知道它繼承什么樣的 class。因為 MsgSender<Company> 中的 Company 是個 template 參數,不到后來(當 LoggingMsgSender 被具現化)無法確切知道它是什么。而如果不知道 Company 是什么,就無法知道 class MsgSender<Company> 看起來是個什么樣 —— 更明確的說是沒辦法知道它是否有個 sendClear 函數。
為了讓問題具體化,假設有個 class CompanyZ 只是用加密通信:
class CompanyZ {
public:
void sendEncrypted(const std::string& msg);
};
一般性的 MsgSender template 對 CompanyZ 并不合適,因為那個 template 提供了一個 sendClear 函數(其中針對其類型參數 Company 調用了 sendCleartext 函數),而這對 CompanyZ 對象并不合理。與糾正這個問題,我們可以針對 CompanyZ 產生一個 MsgSender 特化版;
template<> //一個全特化的
class MsgSender<CompanyZ>{ // MsgSender;它和一般 template 相同
public: //差別只在于它刪掉了 sendClear
void sendSecret(const MsgInfo& info)
{
...
}
};
注意 class 定義式最前頭 “template<>” 語法象征這既不是 template 也不是標準 class,而是個特化版的 MsgSender template,在 template 實參是 CompanyZ 時被使用。這事模板全特化(total template specialization):template MsgSender 針對類型 CompanyZ 特化了,而且其特化是全面性的,也就是說一旦類型參數被定為 CompanyZ,再沒有其他 template 參數可供變化。
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
void sendClearMsg(const MsgInfo& info)
{
將傳送前信息寫至log;
sendClear(info);// 如果 Company==CompanyZ,這個函數就不存在
}
};
那就是為什么 C++ 拒絕這個調用的原因:它知道 base class template 可能被特化,而那個特化版本可能不提供和一般屬性 template 相同的接口。因此它往往拒絕在 templatized base class(模板化基類,MsgSender<Company>)內尋找繼承而來的名稱(本例的 SendClear)。從 Object Oriented C++ 跨進 Template C++ 繼承就不想以前那般暢通無阻了。
我們必須令 C++ “進入 templatized base classes 觀察”。有三個辦法:
第一個辦法是 base class 函數調用動作之前加上 “this->”:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
void sendClearMsg(const MsgInfo& info)
{
將傳送前信息寫至log;
this->sendClear(info); //成立,假設sendClear將被繼承
}
};
第二個辦法是使用 using 聲明式:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
using MsgSender<Company>::sendClear; // 告訴編譯器,請他假設 sendClear 位于 base class 內
void sendClearMsg(const MsgInfo& info)
{
將傳送前信息寫至log;
sendClear(info); //成立,假設sendClear將被繼承
}
};
這里的 using 聲明式不是條款 33 中 “base class 名稱被 derived class 名稱遮掩”,而是編譯器不進人 base class 作用域查找,于是我們通過 using 告訴它,請他這么做。
第三個做法是,明白指出被調用的函數位于 base class 內:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
void sendClearMsg(const MsgInfo& info)
{
將傳送前信息寫至log;
MsgSender<Company>::sendClear(info); //成立,假設sendClear將被繼承
}
};
但這往往不是令人滿意的一個解法,因為如果被調用的是 virtual 函數,上述的明確資格修飾 MsgSender<Company>:: 會關閉 virtual 綁定行為。
從名稱可視點的角度出發,上述每個解法做的事情都相同:對編譯器承諾 “base class template 的任何特化版本都將支持其一般化版本所提供的接口”。這樣一個承諾是編譯器在解析(parse)像 LoggingMsgSender 這樣的 derived class template 時需要的。但如果這個承諾最終未被實踐出來,往后的編譯器最終還是會給事實一個公道。例如,如果稍后的源碼內含這個:
LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
zMsgSender.sendClearMsg(msgData); // 錯誤!無法通過編譯。
因為在那個點上,編譯器知道 base class 是個 template 特化版本 MsgSender<CompanyZ>,而它們知道那個 class 不提供 sendClear 函數,而這個函數卻是 sendClearMsg 嘗試調用的函數。
根本而言,面對 “指涉 base class members” 之無效的 references,編譯器的診斷時間可能發生在早期(當解析 derived class template 的定義式時),也可能發生在晚期(當那些 templates 被特定之 template 實參具現化時)。C++ 的政策是寧愿早診斷。這就是為什么 “當 base classes 從 templates 中被具現化時” 它假設它對那 base classes 的內容毫無所悉的緣故。
請記住:
可在 derived class template 內通過 “this->” 指涉 base class template 內的成員名稱,或藉由一個明白寫出的 “base class 資格修飾符” 完成。