設計與聲明
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著,孫建春、韋強譯