天天看點

《Effective C++》讀書筆記(六) 設計與聲明(第二部分)

設計與聲明

Designs and Declarations

條款22:将成員變量聲明為private

              Declare data members private.

        将成員變量聲明為public、protected和private有什麼差別?乍一看反正都聲明了,應該無所謂的。但是,面向對象程式設計(OOP)是一種特殊的、設計程式的概念性方法。最重要的OOP特性有:抽象、封裝和資料隐藏、多态、繼承、代碼可重用性。

        public成員變量,隻要使用類對象的程式,都能直接通路。也就是說,将public成員變量與公有接口放在一起,public成員變量就能被随意修改,也就談不上封裝,在未來編寫大程式的時候,隻要随便一個不小心,修改了public成員變量,就會造成資料混亂,不可預知的大量代碼受到破壞,太多代碼就會是以需要重寫、重新測試、重新編寫文檔、重新編譯。

        而protected成員變量,在《C++ Primer Plus》上說:“關鍵字protected與private相似,在類外隻能用公有類成員來通路protected部分中的類成員。private和protected之間的差別隻有在基類派生的類中才會表現出來。派生類的成員可以直接通路基類的保護成員,但不能直接通路基類的私有成員。”是以,對于外部世界來說,protected與private相似,但對于派生類來說,protected與public相似。使用了protected,就難以避免設計缺陷,隻要與基類和派生類相關。原因同public成員變量:缺乏封裝性。

        private成員變量,外界隻能通過公有成員函數或友元函數來通路。進而避免了随意修改,展現了封裝性,這樣對程式的維護以及資料保護都大有裨益。

        總之,從封裝的角度來說,隻有兩種通路權限:private(提供封裝)和其他(不提供封裝)。

☆切記将成員變量聲明為private。這可賦予客戶通路資料的一緻性、可細微劃分通路控制、允諾限制條件獲得保證,并提供class作者以充分的實作彈性。

☆protected并不比public更具封裝性。

條款23:甯以non-member、non-friend函數替換member函數

              Prefer non-member non-friend functions to member functions.

        最重要的OOP特性有:抽象、封裝和資料隐藏、多态、繼承、代碼可重用性。

        先讨論封裝:如果某些東西被封裝,它就不再可見(不能被通路)。越多東西被封裝,越少人就能看到他。而越少人看到它,就有越大的彈性去改變它,是以那些改變僅僅直接影響看到改變的那些人事物。然而,如果越多的函數可以通路它,資料的封裝性就越低。當然了,條款22說過,要想有封裝性,成員變量隻能聲明為private。而private成員變量隻能通過member函數以及friend函數通路。進而non-friend函數的封裝性比friend函數的封裝性強,因為兩種提供相同機能,但是non-friend函數通路不了private,進而比後者的封裝性強;同理可證non-member與member函數。

        有兩件事值得注意:第一,這個論述隻适用于non-member non-friend函數。friend函數對class private成員的通路權力和member函數相同。第二,隻因在意封裝性而讓函數“成為class的non-member函數”并不意味着它“不可以是另一個class的member函數”

        可以多多留意C++标準程式庫的組織方式。C++标準程式庫并不是擁有單一、整體、龐大的頭檔案,而是有若幹頭檔案,隻要需要某些功能,引用某個頭檔案,并且編寫時聲明std::的某些機能即可。就沒必要寫成一個大的class并寫入各種member函數或friend函數了。

        也就是說,将所有便利函數放在多個頭檔案内但隸屬于同一個名稱空間,意味客戶可以輕松擴充這一組便利函數。他們需要做的就是添加更多non-member non-friend函數到此名稱空間内。這樣就能增加包裹彈性(packaging flexibility)和機能擴充性。

☆甯可靠non-member non-friend函數替換member函數。這樣做可以增加封裝性、包裹彈性(packaging flexibility)和機能擴充性。

條款24:若所有參數皆需類型轉換,請為此采用non-member函數

              Declare non-member functions when type conversions should apply to all parameters.

         令classes支援隐式類型轉換通常是個糟糕的主意,不過有例外。比如建立數值類型的時候。條款22曾經用一個class的例子用來表現有理數:

class Rational
{
public:
    Rational(int numerator=0,int denominator=1);  //構造函數刻意不為explicit
    int numerator() const;      //分子numerator
    int denominator() const;    //分母denominator
private:
    ...
};
           

        然後将member函數放入public内,要注意傳回的是by-value:

class Rational
{
public:
    ...
    const Rational operator*(const Rational& rhs)const;
private:
    ...
};
           

        如果采用混合式算術,會發現隻有一半行得通,而另一半不行:

result=oneHalf*2;            //OK

result=2*oneHalf;            //ERROR

        歸根結底是這個原因:

result=oneHalf.operator*(2)    //OK

result=2.operator*(oneHalf);   //ERROR

        由于這個member函數并沒有找到const Rational operator*(_____, const Rational& rhs)const{ } ,也調用不了non-member版本的operator*,是以member函數不适合參數之間進行類型轉換。也就是說,隻有當參數被列于參數列(parameter list)内,這個參數才是隐式類型轉換的合格參與者。是以第一個表達式的第二個參數能用隐式類型轉換,第二個的就不可以。

        解決問題的方法很簡單:讓operator*成為一個non-member函數,使允許編譯器在每一個實參身上執行隐式類型轉換:

class Rational
{
    ...
};
const Rational operator* (const Rational& lhs,const Rational& rhs)
{
    return Rational(lhs.numerator()*rhs.numerator(),
                    lhs.denominator()*rhs.denominator());
}
           

        還要注意一點是,member函數的反面是non-member函數,而不是friend函數。不能夠隻因為函數不該成為member,就自動讓它成為friend。當然了,本條款适用于Object-Oriented C++,但Template C++仍需斟酌。可見原書條款46

☆如果你需要為某個函數的所有參數(包括被this指針所指的那個隐喻參數)進行類型轉換,那麼這個函數必須是個non-member

條款25:考慮一個不抛異常的swap函數

              Consider support for a non-throwing swap.

         swap用于置換兩對象值,就是把兩對象的值彼此賦予對象。一般情況下的swap函數由STL提供的算法完成。由于swap在太多太多的應用中需要。是以很有必要在不同的情況下讨論該怎麼寫出而不會抛出異常:

namespace std
{
    template<typename T>       //std::swap的典型實作
    void swap(T& a,T& b)
    {
         T temp(a);
         a=b;
         b=temp;
    }
}
           

        預設的swap實作版本十分普通:a複制到temp,b複制到a,temp複制到b。但是對某些類型而言,這些複制動作無一必要;對它們而言swap預設行為等于是把高速鐵路鋪設在慢速小巷弄内。

        有一個非常好用的手法,“以指針指向一個對象,内含真正資料”那種類型。這種設計的常見表現形式是所謂“pimpl手法”(pimpl是"pointer to implementation"的縮寫),如果用這種手法設計Widget class,就像這樣:

class WidgetImpl
{
public:
    ...
private:
    int a,b,c;
    std::vector<double> v;
    ...
};
class Widget            //這個class使用pimpl手法
{
public:
    Widget(const Widget& rhs);             //複制Widget時,令它
    Widget& operator=(const Widget& rhs)   //複制其WidgetImpl對象
    {
         ...
         *pImpl=*(rhs.pImpl);
         ...
    }
    ...
private:
    WidgetImpl* pImpl;           //指針,所指對象内含Widget資料
};
           

         在這個class中,一旦要置換兩個Widget對象值,唯一需要做的就是置換其pimpl指針。但是預設的swap算法不隻是複制三個Widgets,還複制三個Widgetimpl對象,非常缺乏效率。

        要弄一個non-member函數利用pimpl手法來swap,可以令Widget聲明一個名為swap的public成員函數做真正的置換工作,然後将std::swap特化,令它調用該member函數: 

class Widget            //這個class使用pimpl手法
{
public:
    ...
    void swap(Widget& other)
    {
         using std::swap;              //這個聲明很重要
         swap(pImpl,other.pImpl);      //若要置換,直接置換其pImpl指針
    }
    ...
private:
    WidgetImpl* pImpl;           //指針,所指對象内含Widget資料
};
namespace std
{
    template<>
    void swap<Widget>(Widget& a,Widget& b)
    {
         a.swap(b);          //調用其成員函數
    }
}
           

        這種做法不隻能夠通過編譯,還與STL容器有一緻性,因為所有STL容器也都提供有public swap成員函數和std::swap特化版本(用以調用前者)

        以上的Widget和WidgetImpl都是class,而class templates的Widget與WidgetImpl會有所差別。可以試試将兩個類轉換成class templates:

template<typename T>

class WidgetImpl{...};

template<typename T>

class Widget{...};

        但是在特化std::swap時會遇到麻煩:

namespace std

{

        template<typename T>

        void swap(Widget<T>& a,Widget<T>& b)

        {a.swap(b); }

}

        看起來合情合理,卻不合法。究其原因,std是個特殊的命名空間。std的内容完全由C++标準委員會決定,會禁止我們膨脹那些已經聲明好的東西。如果強行在自己的<std>中加入自己寫的東西,很可能會帶來不可預期的行為。畢竟使用std::的情況太多了。

        解決辦法是這樣的:還是聲明一個non-member swap讓它調用member swap,但不再是那個non-member swap聲明為std::swap的特化版本或重載版本。而是将Widget及其相關機能放入namespace WidgetStuff中:

namespace WidgetStuff
{
    ...                      //模闆化的WidgetImpl等等
    template<typename T>     //同前,内含swap成員函數
    class Widget{...};
    ...
    template<typename T>
    void swap(Widget<T>& a,Widget<T>& b)
    {
         a.swap(b);
    }
}
           

        接下來,讨論的就是以function template來交換兩個對象值:

template<typename T>
void doSomething(T& obj1,T& obj2)
{
     using std::swap;    //令std::swap在此函數内可用
     ...
     swap(obj1,obj2);    //為T型對象調用最佳swap版本
     ...
}
           

        那應該調用哪一個swap呢?C++的名稱查找法則(name lookup rules)確定将找到global作用域或T所在之名稱空間内的任何T專屬swap。如果T是Widget并位于名稱空間WidgetStuff内,編譯器會使用“實參取決之查找規則”(argument-dependent lookup)找出WidgetStuff内的swap。如果沒有T專屬之swap存在,編譯器就使用std内的swap,這得感謝using聲明式讓std::swap在函數内曝光。但需要小心的是,别把這一調用添加額外修飾符,因為那會影響C++挑選适當函數。比如std::swap(obj1,obj2);就會強迫編譯器隻認std内的swap(包括其任何template特化),因而不再可能調用一個定義于它處的較适當T專屬版本。

        最後有一個忠告:成員版swap絕不可抛出異常。那是因為swap的一個最好的應用是幫助classes(和class templatess)提供強烈的異常安全性保障,有關叙述在條款29。而且這一限制不适用于非成員版。因為swap預設版本以copy構造函數和copy assignment操作符為基礎,而一般情況下兩者都允許抛出異常。是以當寫下一個自定義swap,往往提供的不隻是高效置換對象值的辦法,而且不抛出異常。一般而言這兩個swap特性是連在一起的,因為高效率的swaps幾乎總是基于對内置類型的操作(例如pimpl手法的底層指針),而内置類型上的操作絕不會抛出異常。

☆當std::swap對你的類型效率不高時,提供一個swap成員函數,并确定這個函數不抛出異常

☆如果你提供一個member swap,也該提供一個non-member swap用來調用前者。對于classes(而非templates),也請特化std::swap

☆調用swap時應針對std::swap使用using聲明式,然後調用swap并且不帶任何“名稱空間資格修飾”。

☆為“使用者定義類型”進行std template全特化是好的,但千萬不要嘗試在std内加入某些對std而言全新的東西。

參考文獻:

1.《Effective C++》3rd   Scott Meyers著,侯捷譯

2.《C++ Primer Plus》5ed   Stephen Prata著,孫建春、韋強譯

繼續閱讀