我們需要一個程式,傳送資訊到不同的公司去。資訊要不譯成密碼,要不就是未加工的文字。如果編譯期間我們有足夠資訊來決定哪一個資訊傳至那一家公司,就可以採用基於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的內容毫無所悉的緣故。