1. 程式人生 > 其它 >Effective C++條款27:實現——儘量少做轉型動作

Effective C++條款27:實現——儘量少做轉型動作

技術標籤:effective c++c++

一、資料型別轉換語法

C語言風格的型別轉換格式

C++新式轉型

二、建議使用C++新式轉型

  • 舊式轉型雖然雖然合法,但是程式設計時應該儘量使用新式轉型。原因如下:
    • ①新式轉型在程式碼中很容易被識別出來
    • ②新式轉型使轉型動作的目標比較有限。例如你打算將常量性移除,那麼只能只用const_cast

舊式轉型的使用

  • 唯一使用舊式轉型的場景就是,呼叫一個explicit建構函式將一個物件傳遞給一個函式時
  • 例如:
class Widget {

public:
    explicit Widget(int size);
    ...
};

void doSomeWork(const Widget& w);
doSomeWork(Widget(15));                //函式風格
doSomeWidget(static_cast<Widget>(15)); //c++風格

三、對轉型的一種誤解

  • 很多人認為轉型其實什麼都沒做,只是告訴編譯器把某種型別視為另一種型別而已,其實這是錯誤的觀念
  • 任何一種型別轉換(不論是顯式還是隱式,或是編譯器自身完成的隱式轉換),編譯器在編譯出執行期間執行的程式碼時都要做相應的改變

演示案例

  • 下面程式碼將int型別轉換為double型別,x的儲存結構肯定會改變
int x, y;

double d = static_cast<double>(x) / y;

演示案例

  • 上面將派生類轉換為基類,但有時上述兩個指標值並不相同。這時候會有個偏移量在執行期被施行於Derived*指標身上,用以取得正確的Base*指標值
class Base {};

class Derived :public Base {};


int main()

{

    Derived d;

    Base* pb = &d; //派生類轉換為基類


    return 0;

}

四、關於轉型的一個錯誤演示案例

  • 例如許多應用框架都要求在派生類的virtual函式中第一個動作就是先呼叫基類的對應函式。例如下面我們有一個Windows類以及它的派生SpecialWindow,其中onResize()是虛擬函式
class Window {

public:

    virtual void onResize() {}

};


class SpecialWindows :public Window {

public:

    virtual void onResize() {

        //嘗試呼叫基類的虛擬函式,語法雖然沒錯,但是邏輯是錯誤的(程式會產生不明確行為)

        static_cast<Window>(*this).onResize();

    }

};
  • 為什麼上面的型別轉換是錯誤的:
    • 我們在函式中將*this轉換為基類型別,然後嘗試呼叫onResize()虛擬函式,但是該轉型動作產生的實際上是一個“this物件之基類成分”的一個副本,然後在這個副本上呼叫onResize函式
    • 所以當在副本上面呼叫onResize()函式對於this物件根本沒有任何的影響,於是客戶端程式以為呼叫了onResize()函式使物件本身改變了,但是實際上沒有改變,只是改變了一個臨時物件而已
  • 替代方案:就是在虛擬函式中顯式的呼叫基類的函式。例如將上面派生類的虛擬函式更改為下面的形式就是對的了
class SpecialWindows :public Window {

public:

    virtual void onResize() {

        Window::onResize(); //呼叫Window::onResize作用於*this身上

    }

};

五、關於dynamic_cast的一些概述

  • dynamic_cast主要用於:想要使用派生類的方法,但是此時只有一個基類的指標/引用,此時可以使用該型別轉換將基類的指標轉換為派生類指標
  • 為什麼不建議使用該轉型:
    • 因為該轉換會使程式碼執行的非常慢
    • 有的編譯器對於該轉型的實現原理是“基於class名稱的字串比較”,也就是說如果在繼承體系很多的物件身上應用這個型別轉換,那麼會有很多的strcmp的呼叫來比較class名稱
  • 下面是一個dynamic_cast的演示案例,只有派生類有一個閃爍效果的blink()函式,此時我們想用基類指標訪問這個函式
class Window {

public:

    //...

};


class SpecialWindows :public Window {

public:

    void blink();

};



int main()

{

    std::vector<std::tr1::shared_ptr<Window> > VPW;


    //遍歷容器中的每個Window物件並且呼叫blink函式
    
    for (auto iter = VPW.begin(); iter != VPW.end(); ++iter) {

        //使用dynamic_cast轉型,get()函式是獲取shared_ptr中的Window物件指標的意思

        if (SpecialWindows* psw = dynamic_cast<SpecialWindows*>(iter->get())) {

            psw->blink();

        }

    }

    return 0;

}

替代dynamic_cast方法①

  • 對於上面的演示案例我們給出了下面的一種替代方法:直接使用容器儲存派生類物件,不儲存基類物件
  • 缺點:但是這種方法可能需要為每一種派生類都建立一個容器來儲存
int main()

{

    std::vector<std::tr1::shared_ptr<SpecialWindows> > VPW;


    for (auto iter = VPW.begin(); iter != VPW.end(); ++iter) {

        (*iter)->blink();
    
    }

    return 0;

}

替代dynamic_cast方法②

  • 第二種方法是:當你在基類中想做一些事情,那麼也就基類中同時宣告一份,並且將函式設定為virtual的(但是基類中的虛擬函式什麼都不做)

  • 程式碼如下:


class Window {

public:

    virtual void blink() { //條款34告訴你預設實現程式碼可能是個餿主意

        //什麼都不做

    }
    
};


class SpecialWindows :public Window {

public:

    virtual void blink() {

        //實現相關程式碼

    }

};


int main()

{

    std::vector<std::tr1::shared_ptr<Window> > VPW;


    for (auto iter = VPW.begin(); iter != VPW.end(); ++iter) {

        (*iter)->blink();

    }

    return 0;

}

  • 絕對必須避免的一件事情就是所謂的“連串dynamic_cast”,例如下面的程式碼,下面的程式碼執行又大又慢,而且基礎不穩定,因為每次Window類繼承體系一旦改變,所有的程式碼都需要再次更改
class Window {};


class SpecialWindows :public Window {};

class SpecialWindows2 :public Window {};

class SpecialWindows3 :public Window {};

//...


int main()

{

    std::vector<std::tr1::shared_ptr<Window> > VPW;


    for (auto iter = VPW.begin(); iter != VPW.end(); ++iter)

    {

        if (SpecialWindows *psw1 = dynamic_cast<SpecialWindows*>(iter->get())) {

            //...

        }

        else if (SpecialWindows2 *psw2 = dynamic_cast<SpecialWindows2*>(iter->get())) {

            //...

        }

        else if (SpecialWindows3 *psw3 = dynamic_cast<SpecialWindows3*>(iter->get())) {
    
            //...

        }

        //...

    }

    return 0;

}

五、總結

  • 如果可以,儘量避免轉型,特別是在注重效率的程式碼中避免dynamic_cast。如果有個設計需要轉型動作,試著發展無需轉型的替代設計
  • 如果轉型是必須的,試著將它隱藏於某個函式背後。客戶隨後可以呼叫該函式,而不需要將轉型放進它們自己的程式碼
  • 寧可使用新式的轉型,不要使用舊式轉型。前者很容易辨別出來,而且也比較有著分門別類的職掌