(Effective C++)第四章 設計與聲明(Design and declaration)

6.1 條款18:讓接口容易被正確使用,不易被誤用(Make interfaces easy to use correctly and hard to use incorrectly)

條款13表明客戶把新申請的資源置入智能指針中,以免資源泄露。而std::tr1::shared_ptr提供的某個構造函數接收兩個實參,一個是被管理的指針,另一個是引用次數變成0時將被調用的“刪除器“。想這樣:
std::tr1::shared_ptr< Investment> createInvestment()
{
//建立一個NULL shared_ptr並以getRidOfInvestment爲刪除器
std::tr1::shared_ptr< Investment>   retVal(static_cast(Investment*)(0),
getRidOfInvestment);
     retval = …; //令retval指向正確對象
     return retval;
}
示例6-1-1  shared_ptr的一個構造函數
tr1::shared_ptr有個特別好的性質是:它會自動調用它的“每個指針專屬的刪除器“,因而消除另一個潛在的客戶錯誤:所謂的”cross-DLL problem“。
好的接口很容易被正確使用,不容易被誤用。促進正確使用的辦法包括接口的一致性,以及與內置類型的行爲兼容。

6.2 條款19:設計class猶如設計type(Treat class design as type design)

    略過。見《Effective C++》中文 第三版 P84.
    

6.3 條款20:寧以pass-by-reference-to-const 替換pass-by-value (Prefer pass-by-reference-to-const to pass-by-value)

這個規則並不適用於內置類型,以及STL的迭代器和函數對象。對它們而言,pass-by-value往往比較恰當。
缺省情況下,C++是以by value方式傳遞。
class Person {
public:
       Person();
       Virtual ~Person();
Private:
       std::string name;
std::string address;
};
class Student: public Person
{
public:
Student();
~Student();
Private:
       std::string schoolName;
std::string schoolAddress;
};
bool validateStudent(Student s);
Student plato;
bool platoIsOk= validateStudent(plato); //以by value傳遞方式調用函數
示例6-3-1  以by value方式傳遞參數
以plato爲藍本,對此函數的傳遞成本是“一次Student copy構造函數調用,加上一次Student析構函數調用“。而Student和Person有四個string對象,這樣算起來有”六次構造和六次析構“。
更加高效的傳遞方式是pass by reference-to-const。
bool validateStudent(const Student &s);
這種傳遞方式沒有任何構造函數或析構函數被調用。而const是必要的,因爲不這樣的話,調用者會憂慮validateStudent會不會改變他們傳入的那個Student。
使用by reference 方式傳遞參數可以避免slicing(對象切割)問題。當一個derived class對象以by value方式傳遞被視爲一個base class對象,只有base class的copy構造函數會被調用,切割了derived class對象的特性。
class Window {
public:
      std::string name() const;
      virtual void display() const;
};
class WindowWithScrollBars: public Window
{
public:
virtual void display() const;
};
void printNameAndDispaly(Window w)
{
  std::cout<< w.name();
w. display();
}
WindowWithScrollBars wwsb;
printNameAndDispaly(wwsb);  //以by value方式切割了特性
//正確的方式
void printNameAndDispaly(const Window & w)
{
  std::cout<< w.name();
w. display();
}
示例6-3-1  以by value方式傳遞參數

6.4 條款21:必須返回對象時,別妄想返回其reference(Don 't try to return a reference when you must return an object)

考慮到一個用以表現有理數(rational numbers)的class,內含一個函數用來計算兩個有理數的成績。
class Rational {
public:
       Rational (int numerator = 0, int denominator = 1)();//不聲明爲explicit why?
Private:
       int n,d;
friend const Rational operator*(const Rational &lhs, const Rational &rhs);
};
Rational a(1,2);  //a =1/2
Rational b(3,5);  //b=3/5
Rational c= a*b; //c=3/10
示例6-4-1  Rational類
期望“原本就存在一個其值爲3/10的Rational對象“。
第一種方式:在stack空間創建。
const Rational operator*(const Rational &lhs, const Rational &rhs)
{
  Rational result(lhs.n*rhs.n, lhs.d*rhs.d);
  return result;
}
示例6-4-2  Rational類在stack空間創建
這個函數返回一個reference指向的result,但是result是個local對象,而local對象在函數退出前被銷燬了。
第二種方式:在heap空間創建。
const Rational operator*(const Rational &lhs, const Rational &rhs)
{
  Rational *result= new Rational(lhs.n*rhs.n, lhs.d*rhs.d);
  return *result;
}
Rational w,x,y,z;
w = x*y*z; //new了兩次,如何釋放兩次
示例6-4-3  Rational類在heap空間創建
同一個語句調用了兩次operator*,因而調用了兩次new,就需要兩次delete。但是如何delete兩次呢?
const Rational operator*(const Rational &lhs, const Rational &rhs)
{
  static Rational result;
result = (lhs.n*rhs.n, lhs.d*rhs.d);
  return result;
}
示例6-4-4  Rational類在stack空間創建static對象
上述函數不是多線程安全性函數。如果出現下列代碼:
bool operator==(const Rational &lhs, const Rational &rhs)
Rational a,b,c,d;

if ((a*b)==(c*d))
{
}else{
}
不管怎麼樣,表達式(a*b)==(c*d)總是爲真,不論a,b,c,d是什麼值。因爲調用端看到的永遠是static Rational對象的現值。
正確的寫法:
Rational operator*(const Rational &lhs, const Rational &rhs)
{
return Rational(lhs.n*rhs.n, lhs.d*rhs.d);
}
示例6-4-5  Rational類的operator*返回一個對象
當你必須在返回一個reference和返回一個對象之間抉擇時,你的工作就是挑出行爲正確的那個。

6.5 條款22:將成員變量聲明爲private(Declare data members private)

略過。見《Effective C++》中文 第三版 95.

6.6 條款23:寧以non-member、non-friend替換member函數 (Prefer non-member non-friend functions to member functions)

假設有這樣的類,用來清除下週元素緩存區,清除訪問過的URL的歷史記錄和移除系統中的所有cookies。
class WebBrowser {
public:
void clearCache();
void clearHistory();
void removeCookies();
void clearEverything();  //成員函數調用前三個函數
};
void clearEverything(WebBrowser & wb)  //non-menber函數
{
wb. clearCache();
wb. clearHistory();
wb. removeCookies();
}
示例6-6-1  Rational類
我們推崇封裝的原因:它使我們能夠改變事物而隻影響有限客戶。能夠訪問private成員變量的只有class的member函數加上friend函數而已。如果一個member函數和一個non-member,non-friend函數之間做抉擇,而且提供相同機能,那麼,導致較大封裝的是non-member,non-friend函數,因爲它並不增加“能夠訪問class內的private成分“的函數數量。
注意事項
第一,這個論述只適用於non-member,non-friend函數。friend函數對class private成員的訪問權利和member函數相同。
第二,只因在意封裝性而讓函數“成爲class的non-member“,並不意味它”不可以是另一個class的member“。
在C++,比較自然的做法是讓clearBrowser成爲一個non-member函數並且位於WebBrowser所在的同一個namespace內:
namespace WebBrowserStuff{
class WebBrowser{…};
void clearBrowser(WebBrowser &wb);

}

6.7 條款24:若所有參數皆需類型轉換,請爲此採用non-member 函數(Declare non-member functions when type conversions should apply to all parameters)

假設你設計一個class類用來表現有理數,允許整數“隱式轉換“爲有理數似乎頗爲合理。
class Rational {  //允許int-to-Rational隱式轉換
public:
       Rational (int numerator = 0, int denominator = 1)();//不聲明爲explicit why?
       int numerator() const;
       int denominator() const;
const Rational operator*(const Rational &rhs) const; //條款3,20和21
};

Rational oneEight(1,8);  //a =1/8
Rational oneHalf(1,2);  //b=1/2
Rational result = oneEight * oneHalf; //很好
result = result * oneEight;         //很好
示例6-7-1  Rational類
然而當你嘗試混合算術,只有一半行得通:
result = oneHalf * 2;    //很好
result = 2*oneHalf ;     //錯誤
乘法應該滿足交換律,爲什麼?請看:
result = oneHalf.operator * (2);    //很好
result = 2.operator *(oneHalf) ;     //錯誤
是的,oneHalf是一個內含operator *函數的class的對象,所以編譯器調用該函數。然而2整數並沒有相應的class,也沒有operator*成員函數。編譯器也會嘗試尋找可被如下這般調用的non-member operator*(也就是命名空間內或global作用域內):
result = operator *(2, oneHalf) ;     //錯誤
實際上並不存在這樣一個接受int和Rational作爲參數non-member operator *函數,所以查找失敗。
還有,因爲涉及non-explicit構造函數,編譯器纔會使result = oneHalf * 2合法,讓2轉換爲Rational對象。如果Rational構造函數是explicit,以下語句沒有一個編譯通過。
result = oneHalf * 2;    //錯誤 (在non-explicit構造函數的情況下沒問題)
result = 2*oneHalf ;     //錯誤 (甚至在non-explicit構造函數的情況下也是錯誤)
結論是,只有當參數被列爲參數列內,這個參數纔是隱式類型轉換的合格參與者。

如果非要支持乘法交換律,就讓operator*成爲一個non-member函數,就允許編譯器在每一個實參身上執行隱式類型轉換。
class Rational {  //允許int-to-Rational隱式轉換
public:
       Rational (int numerator = 0, int denominator = 1)();//不聲明爲explicit why?
       int numerator() const;
       int denominator() const;
};
const Rational operator*(const Rational &lhs,
const Rational &rhs); //現在成了一個non-member
{
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator ()*rhs.denominator());
}
Rational oneFourth(1,4);  //a =1/4
Rational result;
result = oneFourth * 2;         //很好
result = 2 * oneFourth;         //很好,很強大
示例6-7-2  Rational類的non-member
member函數的反面是non-member函數,不是friend函數。

6.8 條款25:考慮寫出一個不拋異常的swap函數 (Consider support for non-throwing swap)

swap 就是將對象的值彼此賦予對方。std::swap典型實現:
namespace std{
template <typename T>
void swap(T&a, T&b){
T temp(a);
a = b;
b = temp;
}
}
示例6-8-1  std::swap的實現
只要T類型的支持copying行爲,swap的功能就能完成。但是對於“以指針指向一個對象,內含真正的數據”這種類型,缺省的swap就沒必要這麼做。這種設計的常見表現形式是所謂“pimpl”手法(pimpl是“pointer to implementation”縮寫)。
class WidgetImpl{ //針對Widget數據而設計的class
  public:

private:        //可能很多數據,意味複製時間很長
int a,b,c;
std::vector(double) v;

};
class Widget{ //針對Widget數據而設計的class
  public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) //複製Widget時,複製WidgetImpl對象
{

    *pImpl = *(rhs.pImpl);
    …
}

private:        
WidgetImpl *pImpl;  //指針指向對象所含數據
};
示例6-8-2  WidgetImpl的設計
一旦要置換兩個Widget對象值,唯一做的是要置換其pImpl指針,但swap不知道,所以它不僅複製了三個Widgets,還複製了三個WidgetImpls對象。非常缺乏效率。
解決這個問題,要將std::swap針對Widget特化。
class Widget{ //增加swap函數
  public:
void swap(Widget & other)
{
    std::swap(pImpl, other.pImpl);
}

};
namespace std{
template <>  //修訂後的std::swap特化版本
void swap<Widget>(T&a, T&b){
a.swap(b);
}
}
示例6-8-3  std::swap的特化版本
函數一開始“template <>”表示它是std::swap的一個全特化版本,函數之後的“<Widget>”表示這一特化版本針對“T是Widget”而設計的。我們可以將特化版本聲明爲friend,就可以訪問Widget的成員變量,但是也可以在Widget類中聲明一個public成員函數swap。

假設Widget和WidgetImpl都是class template而不是classes。如:
template<typename T>
Class WidgetImpl {…};
template<typename T>
class Widget {…};
在Widget內放個swap成員函數就像以往一樣簡單,但是卻在std::swap時遇上亂流。
namespace std{
template <typename T>  //再次修訂後的std::swap特化版本
void swap<Widget<T> >( Widget<T>&a,   //不合法
Widget<T>&b)
{
a.swap(b);
}
}
示例6-8-4  std::swap的不合法的特化版本
我們在企圖特化(partially sepcialize)一個function template(std::swap),但是C++只允許對class template 偏特化,在function templates身上是行不通的。這段不該通過編譯的。而慣例做法是,簡單地爲它加一個重載版本。
namespace std{
template <typename T>  // std::swap的重載版本
void swap (Widget<T>&a,  Widget<T>&b)
{
a.swap(b);
}
}
示例6-8-5  std::swap的重載版本
一般而言,重載function templates沒有問題,但是std是個特殊的命名空間,不允許添加新的templates到std裏面。正確的做法是聲明一個non-member swap讓它調用成員swap。
namespace WidgetStuff{
template<typename T>
Class WidgetImpl {…};
template<typename T>
class Widget {…};
template <typename T>  //非std空間的non-member版本
void swap (Widget<T>&a,  Widget<T>&b)
{
a.swap(b);
}
}
示例6-8-6  std::swap的重載版本
這個做法對classes和class templates都行得通。
一旦編譯器看到對swap的調用,它們便查找合適的swap並調用之。C++名稱查找法則,確保將找到global作用域或T所在的命名空間內的任何T專屬swap。如果T是Widget並位於命名空間WidgetStuff內,編譯器會根據實參取決之查找規則調用WidgetStuff的swap。如果沒有T專屬的swap存在,編譯器會調用std的swap。
注意事項
當std::swap對你的類型效率不高時,提供一個swap成員函數,並確定這個函數不拋出異常。
如果你提供一個member swap,也應提供一個non-member來調用前者。對於classes(非templates),也請特化std::swap。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章