1. 程式人生 > >Item 43:訪問模板基類中的名稱

Item 43:訪問模板基類中的名稱

Item 43: Know how to access names in templatized base classes.

從面相物件C++轉移到模板C++時,你會發現類繼承在某些場合不在好使了。 比如父類模板中的名稱對子類模板不是直接可見的,需要通過this->字首、using或顯式地特化模板父類來訪問父類中的名稱。

因為父類模板在例項化之前其中的名稱是否存在確實是不確定的,而C++偏向於早期發現問題(early diagnose),所以它會假設自己對父類完全無知。

編譯錯的一個例子

一個MsgSender需要給多個Company傳送訊息,我們希望在編譯期進行型別約束,於是選擇了模板類來實現MsgSender。

template<typename Company>
class MsgSender{
public:
    void sendClear(const MsgInfo& info){...}    // 傳送明文訊息
    void sendSecret(const MsgInfo& info){...}   // 傳送密文訊息
};

由於某種需求我們需要繼承MsgSender,比如需要在傳送前紀錄日誌:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public
: void sendClearMsg(const MsgInfo& info){ // 儲存一些日誌 sendClear(info); // 編譯錯! } };

首先要說明這裡我們建立了新的方法sendClearMsg而不是直接重寫sendClear是一個好的設計, 避免了隱藏父類中的名稱,見Item 33;也避免了重寫父類的非虛擬函式,見Item 36

編譯錯誤發生的原因是編譯器不知道父類MsgSender中是否有一個sendClear,因為只有當Company確定後父類才可以例項化。 而在解析子類LoggingMsgSender時父類MsgSender還沒有例項化,於是這時根本不知道sendClear是否存在。

為了讓這個邏輯更加明顯,假設我們需要一個公司CompanyZ,由於該公司的業務只能傳送密文訊息。所以我們特化了MsgSender模板類:

template<>
class MsgSender<CompanyZ>{
public:
    void sendSecret(const MsgInfo& info){...}   // 沒有定義sendClear()
};

template<>意味著這不是一個模板類的定義,是一個模板類的全特化(total template specialization)。 我們叫它全特化是因為MsgSender沒有其它模板引數,只要CompanyZ確定了MsgSender就可以被例項化了。

現在前面的編譯錯誤就更加明顯了:如果MsgSender的模板引數Company == CompanyZ, 那麼sendClear()方法是不存在的。這裡我們看到在模板C++中繼承是不起作用的。

訪問模板父類中的名稱

既然模板父類中的名稱在子類中不是直接可見的,我們來看如何訪問這些名稱。這裡介紹三種辦法:

this指標

父類方法的呼叫語句前加this->:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
    void sendClearMsg(const MsgInfo& info){
        ...
        this->sendClear(info);
    }
};

這樣編譯器會假設sendClear是繼承來的。

using 宣告

把父類中的名稱使用using宣告在子類中。該手法我們在Item 33中用過,那裡是為了在子類中訪問被隱藏的父類名稱, 而這裡是因為編譯器不會主動去搜索父類的作用域。

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
    using MsgSender<Company>::sendClear;  
    void sendClearMsg(const MsgInfo& info){
        ...
        sendClear(info);
    }
};

using語句告訴編譯器這個名稱來自於父類MsgSender。

呼叫時宣告

最後一個辦法是在呼叫時顯式指定該函式所在的作用域(父類):

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
    void sendClearMsg(const MsgInfo& info){
        ...
        MsgSender<Company>::sendClear(info);
    }
};

這個做法不是很好,因為顯式地指定函式所在的作用域會禁用虛擬函式特性。萬一sendClear是個虛擬函式呢?

子類模板無法訪問父類模板中的名稱是因為編譯器不會搜尋父類作用域,上述三個辦法都是顯式地讓編譯器去搜索父類作用域。 但如果父類中真的沒有sendClear函式(比如模板引數是CompanyZ),在後續的編譯中還是會丟擲編譯錯誤。