swap是一個(gè)非常有趣的程序。它起初是作為STL的一部分引入C++的,而后就成為了異常安全編程的一個(gè)重要的支柱(參見(jiàn)條目29),同時(shí)對(duì)于可以自賦值的對(duì)象而言它還是一個(gè)常用的復(fù)制處理機(jī)制。由于swap如此神通廣大,那么以一個(gè)恰當(dāng)?shù)姆绞饺?shí)現(xiàn)它就顯得十分重要了,但是它舉足輕重的地位也決定了實(shí)現(xiàn)它并不是一件手到擒來(lái)的事情。在本條目中,我們就會(huì)針對(duì)swap函數(shù)展開(kāi)探索,逐步掌握如何去駕馭它。
swap函數(shù)的功能是交換兩個(gè)對(duì)象的值。在默認(rèn)情況下,交換工作是通過(guò)標(biāo)準(zhǔn)庫(kù)的swap算法完成的。它的標(biāo)準(zhǔn)實(shí)現(xiàn)方式就能精確地完成你所期望的工作:
namespace std {
template<typename T> // std::swap的標(biāo)準(zhǔn)實(shí)現(xiàn)
void swap(T& a, T& b) // 交換a與b的值
{
T temp(a);
a = b;
b = temp;
}
}
只要你的類(lèi)型支持復(fù)制(通過(guò)拷貝構(gòu)造函數(shù)和拷貝復(fù)制運(yùn)算符),那么默認(rèn)的swap實(shí)現(xiàn)就可以讓兩個(gè)該類(lèi)型的對(duì)象互相交換,你不需要專(zhuān)門(mén)做任何工作來(lái)支持這一功能。
然而,你可能對(duì)默認(rèn)的swap實(shí)現(xiàn)抱有諸多不滿(mǎn)。它會(huì)帶來(lái)3次對(duì)象復(fù)制工作:a復(fù)制到temp,b到a,temp到b。對(duì)于一些類(lèi)型來(lái)說(shuō),這些復(fù)制操作并不都是必需的。對(duì)于這些類(lèi)型來(lái)說(shuō),默認(rèn)的swap會(huì)成為你程序的桎梏。
上述的那種類(lèi)型大都符合下面的特征:它的主要成分是一個(gè)指針,這一指針會(huì)指向另一個(gè)類(lèi)型,真實(shí)的數(shù)據(jù)包含在另一個(gè)類(lèi)型中。對(duì)這一設(shè)計(jì)的一種常見(jiàn)的形式是“pimpl慣用法”(pointer to implementation,指向?qū)崿F(xiàn)的指針,參見(jiàn)條目31)。舉例說(shuō)明,Widget類(lèi)可以使用這種設(shè)計(jì)模式:
class WidgetImpl { // 保存Widget的數(shù)據(jù)的類(lèi)
public: // 細(xì)節(jié)不重要
...
private:
int a, b, c; // 可能會(huì)有很多數(shù)據(jù)
std::vector<double> v; // 復(fù)制它們的代價(jià)是很高的!
...
};
class Widget { // 使用pimpl慣用法的類(lèi)
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) // 要復(fù)制一個(gè)Widget對(duì)象,只要
{ // 復(fù)制對(duì)應(yīng)的WidgetImpl對(duì)象。
... // 關(guān)于operator=實(shí)現(xiàn)的一般信息
*pImpl = *(rhs.pImpl); // 參見(jiàn)條目10、11、12
...
}
...
private:
WidgetImpl *pImpl; // 指向包含當(dāng)前Widget數(shù)據(jù)的對(duì)象
};
為了交換兩個(gè)Widget對(duì)象的值,我們所要做的僅僅是交換他們的pImpl指針,但是默認(rèn)的swap算法是不可能知道這一切的,它不僅會(huì)復(fù)制三個(gè)Widget對(duì)象,同時(shí)也會(huì)復(fù)制三個(gè)Widget對(duì)象。這樣做效率太低了。
我們要做的是告訴std::swap當(dāng)交換Widget時(shí),執(zhí)行的交換操作應(yīng)當(dāng)僅僅針對(duì)它們內(nèi)部的pImpl指針。有一種精確的說(shuō)法來(lái)描述這一方法:將Widget的std::swap特化。下面是基本的思想,盡管以這種方式不能通過(guò)編譯:
namespace std {
template<> // 在T為Widget時(shí),
void swap<Widget>(Widget& a, // 這是std::swap的一個(gè)特化版本
Widget& b) // 這段代碼不能通過(guò)編譯
{
swap(a.pImpl, b.pImpl); // 要交換兩個(gè)Widget,
} // 只需要交換它們的pImpl指針
}
程序開(kāi)端的“template<>”告訴我們這是std::swap的一個(gè)完全特化模板,函數(shù)名后面的“<Widget>”告訴我們當(dāng)前的特化針對(duì)T是Widget的情況。換種說(shuō)法,當(dāng)一般的swap模板應(yīng)用于Widget時(shí),應(yīng)當(dāng)使用這一具體實(shí)現(xiàn)。一般情況下,我們沒(méi)有權(quán)限去改動(dòng)std名字空間內(nèi)部的內(nèi)容,但是我們有權(quán)針對(duì)我們自己創(chuàng)建的類(lèi)型(比如Widget)來(lái)完整地特化標(biāo)準(zhǔn)模板(就像swap)。這就是我們所要做的。
然而,就像我說(shuō)過(guò)的,這段代碼是不能通過(guò)編譯的。這是因?yàn)樗鼑L試訪問(wèn)a與b內(nèi)部的pImpl指針,但是它們是私有的。我們可以將我們的特化函數(shù)聲明為友元,但這里的規(guī)則有些不同:這里要求我們讓Widget包含一個(gè)名為swap的公共成員函數(shù),讓這個(gè)swap進(jìn)行實(shí)際的交換工作,然后特化std:swap來(lái)調(diào)用這一成員函數(shù)。
class Widget { // 同上,
public: // 僅添加了一個(gè)swap成員函數(shù)
...
void swap(Widget& other)
{
using std::swap; // 本節(jié)后面會(huì)解釋為什么這樣聲明
swap(pImpl, other.pImpl); // 交換pImpl指針來(lái)交換Widget
}
...
};
namespace std {
template<> // 特化的std::swap (已修正)
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b); // 要交換Widget,
} // 只要調(diào)用它們的swap成員函數(shù)
}
這樣的代碼不僅僅可以通過(guò)編譯,而且也與STL容器相協(xié)調(diào),它不僅僅提供了公有的swap成員函數(shù),而且還提供了特化的std::swap來(lái)調(diào)用這些成員函數(shù)。
然而,我們不難發(fā)現(xiàn),Widget和WidgetImpl都是類(lèi)模板,而不是類(lèi),似乎我們可以自定義WidgetImpl中保存的數(shù)據(jù)的類(lèi)型:
template<typename T> class WidgetImpl { ... };
template<typename T> class Widget { ... };
將一個(gè)swap成員函數(shù)放入Widget中(如果需要,也可以是WidgetImpl)仍然十分簡(jiǎn)單,但是我們對(duì)std::swap特化時(shí)將會(huì)遇到問(wèn)題。下面是我們希望編寫(xiě)的代碼:
namespace std {
template<typename T>
void swap<Widget<T> >(Widget<T>& a, Widget<T>& b)
// 錯(cuò)誤!非法代碼
{ a.swap(b); }
}
這樣的代碼看上去完美無(wú)瑕,但是它是非法的。因?yàn)槠渲袊L試對(duì)一個(gè)函數(shù)模板(std::swap)進(jìn)行不完全的特化,但是,盡管C++允許對(duì)類(lèi)模板進(jìn)行不完全特化,而函數(shù)模板就不行了。這一代碼不應(yīng)通過(guò)編譯(盡管一些編譯器會(huì)錯(cuò)誤的接受)。
當(dāng)你期望對(duì)一個(gè)函數(shù)模板進(jìn)行“不完全特化”時(shí),通常的做法非常簡(jiǎn)單,就是添加一個(gè)該函數(shù)的重載。代碼可能是下面的樣子:
namespace std {
template<typename T> // std::swap的一個(gè)重載
void swap(Widget<T>& a, Widget<T>& b)
// (注意swap后邊沒(méi)有<...>)
{ a.swap(b); } // 下文解釋了為什么這樣做不合法
}
一般情況下,重載函數(shù)模板是可以的,但是std是一個(gè)很特殊的名字空間,它的規(guī)則也是獨(dú)特的。對(duì)std中的模板進(jìn)行完全特化是合法的,但是為std添加一個(gè)新的模板卻是不合法的(類(lèi)或函數(shù)或其他一切都不可以)。std的內(nèi)容是由C++標(biāo)準(zhǔn)化委員會(huì)一手確定的,我們無(wú)法修改他們所規(guī)定的任何形式,只能“望碼興嘆”。越軌的代碼似乎可以運(yùn)行,但它們的行為卻是未定義的。如果你希望你的代碼擁有可預(yù)知的行為,你就不應(yīng)該在std中添加新的內(nèi)容。
那么應(yīng)該怎么辦呢?我們?nèi)匀恍枰环N方法來(lái)讓其他人通過(guò)調(diào)用swap來(lái)訪問(wèn)我們更加高效的特化版本。答案很簡(jiǎn)單。我們?nèi)匀豢梢酝ㄟ^(guò)聲明一個(gè)非成員函數(shù)swap來(lái)調(diào)用成員函數(shù)swap實(shí)現(xiàn),只要這個(gè)非成員函數(shù)不是std::swap的特化或者重載版本即可。比如說(shuō),如果我們所有與Widget相關(guān)的功能都在名字空間WidgetStuff中,那么代碼看上去應(yīng)該是這樣:
namespace WidgetStuff {
... // 模板化的WidgetImpl,等等
template<typename T> // 同上,包括swap成員函數(shù)
class Widget { ... };
...
template<typename T> // 非成員函數(shù)swap
void swap(Widget<T>& a, Widget<T>& b)
// 不屬于std名字空間
{
a.swap(b);
}
}
現(xiàn)在,如果任意位置的代碼對(duì)兩個(gè)Widget對(duì)象調(diào)用了swap,C++的名字搜尋守則(更具體地說(shuō),就是所謂的參數(shù)依賴(lài)搜尋或Koenig搜尋)將會(huì)在WidgetStuff中查找具體到Widget的版本。這恰恰是我們需要的。
由于這種方法針對(duì)類(lèi)或者類(lèi)模板可以正常運(yùn)行,所以看上去似乎我們應(yīng)該在任何情況下都使用它。但是遺憾的是,我們還是要對(duì)于類(lèi)的std::swap進(jìn)行特化(稍后會(huì)交代理由),所以如果你想要在盡可能多的上下文中(你所需要的)調(diào)用具體到類(lèi)的swap版本,你就需要在你的類(lèi)所在的名字空間編寫(xiě)一個(gè)非成員版本的swap,同時(shí)還需要一個(gè)std::swap的特化版本。
順便說(shuō)一下,即使你沒(méi)有使用名字空間,上述內(nèi)容仍然有效(也就是說(shuō),你仍需要一個(gè)非成員的swap去調(diào)用成員函數(shù)swap),但是為什么你要把所有的類(lèi)、模板、函數(shù)、枚舉類(lèi)型、enumerant、typedef的名字統(tǒng)統(tǒng)塞進(jìn)全局名字空間里呢?如果你對(duì)編程規(guī)范有一點(diǎn)概念的話,都不會(huì)這樣做的。
到目前為止我所介紹的一切內(nèi)容都是以swap的作者的角度展開(kāi)的,但是以一個(gè)客戶(hù)的眼光來(lái)審視一下swap也是很有價(jià)值的。假設(shè)你正在編寫(xiě)一個(gè)函數(shù)模板,這里你需要交換兩個(gè)對(duì)象的值:
template<typename T>
void doSomething(T& obj1, T& obj2)
{
...
swap(obj1, obj2);
...
}
這里應(yīng)該調(diào)用哪一個(gè)swap呢? std中存在一個(gè)通用版本,這是你所知道的;另外std中可能還有一個(gè)針對(duì)這一通用版本的特化版本,它可能存在也可能不存在;或者一個(gè)模板的版本,它可能存在也可能不存在,它是否在一個(gè)名字空間中也不能確定(但可以肯定不在std名字空間中)?此時(shí)你所希望的是,如果存在一個(gè)模板版本的話,就調(diào)用它;如果不存在,就返回調(diào)用std中的通用版本。以下是滿(mǎn)足這一要求的代碼:
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; // 確保std::swap在此函數(shù)中可用
...
swap(obj1, obj2); // 為類(lèi)型T的對(duì)象調(diào)用最佳的swap
...
}
當(dāng)編譯器看到對(duì)swap的調(diào)用時(shí),它們會(huì)尋找恰當(dāng)?shù)?span style="font-family:"Courier New";">swap來(lái)進(jìn)行調(diào)用。C++的名字搜尋原則確保了在全局或T類(lèi)型所在的名字空間中來(lái)查找所有的精確到T的swap。(舉例說(shuō),如果T是位于WidgetStuff名字空間中的Widget,那么編譯器將會(huì)使用參數(shù)依賴(lài)搜尋方式來(lái)查找WidgetStuff中的swap。)如果沒(méi)有精確到T的swap存在,那么編譯器將會(huì)使用std中的swap,多虧了using聲明可以使std::swap在本函數(shù)中可見(jiàn)。然而即使這樣,編譯器也更期望得到一個(gè)精確到T的std::swap的特化版本,而不是未確定類(lèi)型的模板,因此如果std::swap特化為T版本,那么這一特化的版本將會(huì)得到使用。
因此,調(diào)用正確的swap十分簡(jiǎn)單。你所需要關(guān)心的事僅僅是不去限制對(duì)它的調(diào)用,因?yàn)槿绻@樣做會(huì)使C++如何決定去調(diào)用函數(shù)的方式受到影響。舉例說(shuō),如果你用下面的方式調(diào)用了swap:
std::swap(obj1, obj2); // 調(diào)用swap的錯(cuò)誤方法
你強(qiáng)迫編譯器僅僅去考慮std中的swap(包括所有的模板特化版本),這樣做就排除了得到一個(gè)位于其他位置的精確到T版本的swap的可能,即使它是更加合理的。然而,一些進(jìn)入誤區(qū)的程序員還是會(huì)以這種方式限制swap的調(diào)用,這里你就可以看出,為你的類(lèi)提供一個(gè)std::swap的完全特化版本是多么重要:對(duì)于那些使用不恰當(dāng)?shù)木幋a風(fēng)格寫(xiě)出的代碼(這樣的代碼也存在于一些標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)當(dāng)中,如果你感興趣可以自己編寫(xiě)一些代碼,來(lái)幫助這樣的代碼盡可能的提高效率),精確到類(lèi)的swap實(shí)現(xiàn)仍然有效。
此刻,我們已經(jīng)介紹了默認(rèn)的swap、成員swap、非成員swap、std::swap的特化版本,以及對(duì)swap的調(diào)用,現(xiàn)在讓我們來(lái)做一個(gè)總結(jié)。
首先,如果對(duì)你的類(lèi)或者類(lèi)模板使用默認(rèn)的swap實(shí)現(xiàn)能夠得到可以接受的效率,你就不需要做任何事情。任何人想要交換你創(chuàng)建的類(lèi)型的對(duì)象時(shí),都會(huì)去調(diào)用默認(rèn)的版本,此時(shí)可以正常工作。
其次,如果默認(rèn)的swap實(shí)現(xiàn)并不夠高效(大多數(shù)情況下意味著你的類(lèi)或模板正在運(yùn)用pimpl慣用法),請(qǐng)按下面步驟進(jìn)行:
1. 提供一個(gè)公用的swap成員函數(shù),讓它可以高效的交換你的類(lèi)型的兩個(gè)對(duì)象的值。理由將在后面列出,這個(gè)函數(shù)永遠(yuǎn)不要拋出異常。
2. 在你的類(lèi)或模板的同一個(gè)名字空間中提供一個(gè)非成員的swap。讓它調(diào)用你的swap成員函數(shù)。
3. 如果你正在編寫(xiě)一個(gè)類(lèi)(而不是類(lèi)模板),要為你的類(lèi)提供一個(gè)std::swap的特化版本。同樣讓它調(diào)用你的swap成員函數(shù)。
最后,如果你正在調(diào)用swap,要確保使用一條using聲明來(lái)使std::swap對(duì)你的函數(shù)可見(jiàn),然后在調(diào)用swap時(shí),不要做出任何名字空間的限制。
文中還有一處欠缺,那就是本文的標(biāo)題中的敬告:不要讓swap的成員函數(shù)版本拋出異常。這是因?yàn)?span style="font-family:"Courier New";">swap最重要的用途之一就是幫助類(lèi)(或類(lèi)模板)來(lái)提供異常安全的保證。條目29中詳細(xì)介紹了這一點(diǎn),但是這一技術(shù)做出了“swap的成員函數(shù)版本永遠(yuǎn)不會(huì)拋出異常”這一假設(shè)。這一約束僅僅應(yīng)用于成員函數(shù)版本,非成員版本則不受這一限制。這是因?yàn)?span style="font-family:"Courier New";">swap的默認(rèn)版本基于拷貝構(gòu)造和拷貝賦值,而在一般情況下,這兩種函數(shù)都可能拋出異常。因此,當(dāng)你編寫(xiě)一個(gè)自定義版本的swap時(shí),在典型情況下你不僅要提供一條更高效的交換對(duì)象值的方式,同時(shí)你也要提供一個(gè)不拋出異常的版本。作為一條一般的守則,這兩條swap的特征是相輔相成的,因?yàn)楦咝У?span style="font-family:"Courier New";">swap同時(shí)也基于內(nèi)建數(shù)據(jù)類(lèi)型的操作(諸如pimpl慣用法中使用的指針),同時(shí)內(nèi)建數(shù)據(jù)類(lèi)型的操作決不會(huì)拋出異常。
時(shí)刻牢記
l 在對(duì)你的類(lèi)型使用std::swap時(shí)可能會(huì)造成效率低下時(shí),可以提供一個(gè)swap成員函數(shù)。確保你的swap不要拋出異常。
l 如果你提供了一個(gè)swap的成員函數(shù),那么同時(shí)要提供一個(gè)非成員函數(shù)swap來(lái)調(diào)用這一成員。對(duì)于類(lèi)而言(而不是模板),還要提供一個(gè)std::swap的特化版本來(lái)調(diào)用swap成員函數(shù)。
l 在調(diào)用swap時(shí),要為std::swap使用一條using聲明,然后在調(diào)用swap時(shí),不要做出名字空間的限制。
l 對(duì)用戶(hù)自定義類(lèi)型而言,提供std的完全特化版本不成問(wèn)題,但是決不要嘗試在std中添加全新的內(nèi)容。