1. 程式人生 > >[C++]關於介面的設計與宣告--對封裝性的理解

[C++]關於介面的設計與宣告--對封裝性的理解

設計與宣告

所謂軟體設計,是“令軟體做出你希望它做的事情”的步驟和方法,通常以頗為一般性的構想開始,最終十足的細節,以允許特殊介面(interface)的開發。這些介面而後必須轉換為C++宣告式。本文討論對良好C++介面的設計和宣告。

1. 讓介面容易被正確使用,不易被誤用

C++擁有許多的介面,function介面,class介面,template介面….每一種介面實施客戶與你的程式碼互動的手段。理想情況下,客戶總是會準確的使用你的介面並獲得理想的結果,而如果客戶錯誤的使用了介面,程式碼就不應該通過編譯。

用結構體限制引數型別

假設我們現在需要做一個表示時間的class

class Date {
public:
    Date(int month, int day, int year);
    ...
};

乍看起來,這個類的建構函式並沒有什麼問題,但其實存在著很多的隱患。我們當然希望使用者可以準確的使用我們的類,但使用者卻有可能因為某些特定的原因無法正確使用我們的類,例如沒有按照月,天,年的順序來完成構造。而此時,為了避免使用者犯錯,我們需要強制使用者按照我們的設計來用這個類:

//  special design
//  預設情況下,struct內部都是public訪問限制。
struct Day {
explicit Day(int d) : val(d) { }
int
val; }; struct Month { explicit Month(int m) : val(d) { } int val; }; struct Year { explicit Year(int y) : val(d) { } int val; }; class Date { public: Date(const Month &m, const Day &d, const Year &y); ... }; Date d1(30, 3, 1996); // error! Date d2(Month(3), Day(30), Year(1996)); // right!

用struct來封裝資料,可以明智而審慎地匯入新型別並預防“介面被誤用”。

一旦型別限定了,限定其值也是合情合理的了。例如一年只有12個月,所以Month應該反映這一點。辦法之一就是用enum表現月份,但enum不具備我們希望的型別安全性,例如enum可以被當做一個int使用。比較安全的做法是:預先定義所有有效的Month。

class Month {
public:
    static Month Jan() { return Month(1); }
    static Month Feb() { return Month(2); }
    ....
    static Month Dec() { return Month(12); }
private:
    explicit Month(int m);
    ..
};

Date d(Month::Mar(), Day(30), Year(1996));

以函式替換物件,表現某個特定的月份是一種相當不錯的方法。

限制類型內什麼能做,什麼不能做

除非有更好的理由,否則儘量讓你的type的行為與內建type一致!

使用者很清楚像int這樣的type有什麼行為,所以你應該努力讓你的type在合情合理的前提下也有相同的操作。例如,如果a和b都是int,那麼對a*b賦值就是不合法的。

避免無端與內建型別不相容,真正的理由是為了==提供行為一致的介面==。很少有其他性質比”一致性“更能導致”介面被正確使用“,也很少有性質比得上”不一致性“更加劇介面的惡化。

2. 設計class猶如設計type

C++就像其他OOP語言一樣,當你定義一個新class,也就定義了一個新的type。包括,過載函式和操作符、控制記憶體的分配和歸還、定義物件的初始化和析構……全都在你控制,因而你應該帶著和“語言設計者當初設計語言內建型別時”一樣的謹慎來設計class。以下給出了部分class設計規範。

  • 新type的物件應該如何被建立和銷燬?這回應該到你如何設計class的建構函式和解構函式以及記憶體分配函式和釋放函式。
  • 物件的初始化和物件的賦值有什麼樣的差別?這決定了你如何設計建構函式和賦值操作符。最重要的是別混淆“初始化”和“賦值”,因為他們對應不同的函式呼叫。
  • 新type物件如果被passed by value,意味著什麼?記住,copy建構函式用來定義一個type的pass by value如何實現。
  • 什麼是新type的“合法值”?這意味你的成員函式必須進行錯誤檢查工作,也影響了函式丟擲的異常、以及函式異常明細列。
  • 你的type需要配合某個繼承體系嗎?如果你繼承自某些既有的class,你就會受到這些class設計的束縛,特別是受到他們的函式是virtual或non-virtual的影響。如果你允許你的class被其他class繼承,那會影戲到你的解構函式是否會virtual。
  • 你的新type需要什麼樣的轉換?因為你的type存在於其他大量的type之間,這決定了你是否需要讓自己type有途徑轉換為其他的type(隱式還是顯式的?)
  • 什麼樣的操作符和函式對此新type而言是合理的?這取決於你的成員函式的設計。
  • 什麼樣的標準函式應該駁回?那些就是你宣告為private的物件。
  • 誰該取用新type的成員?這決定了如何安排函式是public,protected還是private,以及那些函式/類是friend。
  • 什麼是新type的“未宣告介面”?他對效率、異常安全性以及資源運用提供何種保證?
  • 你的新type有多麼一般化?如果你並不是為了定義一個新type而是要定義一整個type家族,那麼應該定義一個新的class template。
  • 你是否真的需要一個新的type?如果你只是為了給base class新增某些功能,那麼定義一個或多個non-member 函式或template,更好。

設計class是一件非常具有挑戰的事情,所以如果你希望設計一個class,最好像設計一個type一樣,把各種問題都思考一遍。

3. 寧以pass by reference to const替換pass by value

在預設情況下C++總是以pass-by-value的方式傳遞物件至函式,實際上,就是傳遞復件,而這些復件都是由copy建構函式產生的,這可能使得pass-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;
};

// in main:
bool checkStudent(Student s);
Student one;
bool whoh = checkStudent(one);

在checkStudent呼叫時,發生了什麼?

這顯然是一個pass-by-value的函式,也就意味著一定會出現copy建構函式,對於此函式而言,引數的傳遞成本是“一次student copy建構函式呼叫,加上一次student解構函式呼叫”。不僅如此,student還繼承於person,所以還有一次person建構函式和person解構函式,以及student裡面的兩個string物件,和person裡面的兩個string物件,總而言之,總體成本就是“六次建構函式和六次解構函式!”多麼可怕的開銷!

問題解決

解決這個問題非常的簡單。只要使用pass by reference to const就可以了。因為by reference不會導致建構函式和解構函式的使用,節省了大量開銷,同時因為是const,也保證了引數不會再函式內被更改。

bool checkStudent(const Student &s);

問題產生2

pass-by-value還會導致物件切割問題(slicing)。當一個dereived class物件以by value方式傳遞並被視為一個base class物件時,bass class的copy建構函式就會被呼叫,而“造成此物件的行為像個derived class物件”的那些特化性質全部被切割掉,只剩下base class物件。這並不奇怪。

class Window {
public:
    ...
    std::string name() const;
    virtual void display() const;
};
class SpecialWindow {
public:
    ..
    virtual void display() const;
};
....
// in main:
void print(Window w) {
    cout << w.name();
    w.display();
}

當你把一個SpecialWindow物件傳遞給void print(Window w)函式時,就像前文所說的,會使得SpecialWindow的特化性質全部被切割掉,於是乎,你本想著輸出SpecialWindow的特別內容結果只輸出了Window內容。

問題解決2

解決這個問題仍然是使用reference。由此來引發動態繫結,從而使用SpecialWindow的display。

void print(const Window& w) {
    cout << w.name();
    w.display();
}

總結

窺視C++編譯器的底層就會發現,實際上reference就是以指標實現出來了,pass by reference通常意味著真正傳遞的是指標。因此,如果你有個物件屬於內建型別(如int),pass-by-value通常來說效率會更好。這對於STL的迭代器和函式物件同樣適用。因為習慣上他們都是設計為pass-by-value。迭代器和函式物件的實踐者都有責任看看他們是否高效且不受切割問題。

有人認為,所有小型type物件都應該適用pass-by-value,甚至對於使用者定義的class。實際上是不準確的。第一,物件小,並不意味著他的copy建構函式開銷小;2)即使是小型物件並不擁有昂貴的copy建構函式,也可能存在效率上的問題,例如某些編譯器不願意把只由一個double組成的物件放進快取器,但如果你使用reference,編譯器一定會把指標(就是reference的實現體)放進快取器。3)作為使用者自定義型別,其大小是很容易被改變的。隨著不斷的使用,物件可能會越來越大。

一般而言,合理假設“pass-by-value更合適”的唯一物件就是內建型別和STL的迭代器和函式物件,其他的最好還是使用by reference。

4. 必須返回物件時,別妄想返回其reference

前面我們討論了pass-by-reference可以提高效率,於是乎,有的人就開始堅定地使用reference,甚至開始傳遞一些refereence指向其實並不存在的物件。

問題產生

此問題產生的理由非常的簡單,就是作者希望可以節省開銷提高效率。並因此而產生大量的錯誤。

class Rational {
public:
    Rational(int num1 = 0, int num2 = 1);
    ...
private:
    int n1, n2;
    friend Rational& operator*(const Rational& lhs, const Rational& rhs);

operator*試圖返回一個引用,併為此尋找合乎邏輯的實現程式碼。

嘗試1:直接返回

Rational& operator*(const Rational& lhs, const Rational& rhs) {
    Rational result(lhs.n1 * rhs.n1, lhs.n2 * rhs.n2);
    return result;
}

問題顯然。因為result是一個on the stack物件,在作用域結束後,物件就被銷燬,於是返回了一個沒有指向的reference。嘗試失敗!

嘗試2:返回on the heap物件

Rational& operator*(const Rational& lhs, const Rational& rhs) {
    Rational* result = new Rational(lhs.n1 * rhs.n1, lhs.n2 * rhs.n2);
    return *result;
}

此程式碼乍看起來似乎沒什麼問題,但其實隱含殺機。你在函式中動態申請了一塊記憶體放這個變數,這也就意味著你必須管理這塊資源(見前文:資源管理)。然而管理這塊資源幾乎不可能,因為你不可能希望在main函式裡一直有一個變數在守著這塊資源並且及時的delete掉。而且當大量使用*操作符時,管理大量的資源根本不可能!就算你有這樣的毅力這麼管理,也不可能希望有使用者願意做這樣的體力活。

嘗試3:使用static變數

Rational& operator*(const Rational& lhs, const Rational& rhs) {
    static Rational result(lhs.n1 * rhs.n1, lhs.n2 * rhs.n2);
    return result;
}

這程式碼乍看起好像又要成功了?!其實並沒有。問題出現的十分隱蔽:

bool operator == (const Rational& lhs, const Rational& rhs);

if ((a*b) == (c*d)) {
    ...
} else {
    ...
}

問題就出在等號操作,等號永遠會成立!因為,在operator == 被呼叫前,已有兩個操作符被呼叫,每一個都返回操作函式內部的static物件,而這兩個物件實際上就是一個物件!(對於呼叫端來說,確實如此!)於是乎,你根本就沒有完成*操作符所應該具備的功能。

問題解決

問題的解決就是,別掙扎了!使用pass-by-value吧。不就是一點建構函式和解構函式的開銷嘛。比起大量的錯誤和記憶體的管理。這點開銷還是很划算的。

class Rational {
public:
    Rational(int num1 = 0, int num2 = 1);
    ...
private:
    int n1, n2;
    friend Rational operator*(const Rational& lhs, const Rational& rhs) {
        return Rational(lhs.n1*rhs.n1, rhs.n2*rhs.n2);
    }

5. 將成員變數宣告為private

在我們最初學習C++ OOP時就有一天準則,成員變數總是要宣告為private。本節我們來討論為何成員變數要被宣告為private。

  • 理由一:語法一致性。
    因為成員變數不是public,客戶唯一能夠訪問物件的辦法就是通過成員函式。如果public介面內的每一樣東西都是函式,客戶就不用糾結呼叫他時是否需要使用小括號。如此便能省下大量的時間。
  • 理由二:使用函式可以讓你對成員變數的處理有更準確的控制。
    如果成員變數是public,那麼每個人都可以對他進行讀寫,但如果你以函式取得或設定其值,就可以實現“不準訪問”,“只讀訪問”,“讀寫訪問”等訪問控制。
    如以下程式碼:
class AccessLevel {
private:
    int noAccess;
    int ReadOnly;
    int WriteOnly;
    int readWrite;
public:
    // ...
    int getReadOnly() {
        return ReadOnly;
    }
    void setWriteOnly(int i) {
        WriteOnly = i;
    }
    void setreadWrite(int i) {
        readWrite = i;
    }
    int readreadWrite() {
        return readWrite;
    }
};

如此精細地對各個資料成員進行訪問限制是有必要的。

  • 理由三:封裝!
    這是最有說服力的理由了!C++ OOP其中最重要的一條性質就是封裝性!將資料成員封裝在介面的後面,可以為“所有可能的實現”提供彈性。
    封裝的重要性比我們最初見到它時更重要。如果我們對客戶隱藏成員變數,就可以確保class的約束條件受到維護,因為只有成員函式可以影響他們。public意味著不封裝,而幾乎可以說不封裝意味著不可改變,特別是對被廣泛使用的class而言。被廣泛使用的class是最需要封裝的一個族群,因為他們能夠從“改採用一個教佳實現版本”中獲益。

我們繼續來討論protected的封裝性。

一般人會認為protected比public更具有封裝性。其實不然。更準確的判斷方法是:某些東西的封裝性與“當其內容改變時可能造成的程式碼破壞量”成反比。所謂改變,也許是從class中移除他。於是乎,我們可以進行以下分析。對於public的成員變數,如果我們移除他,意味著我們要破壞所有使用它的客戶程式碼。(破壞量很大吧?)而對於protected的成員變數呢,如果我們移除它,意味著要破壞所有derived class(破壞量也很大吧?)因此protected和public的封裝性其實是一樣的。這也就意味著,一旦我們決定把某個成員變數宣告為public或protected,就很難改變某個成員變數所涉及的一切。

結論就是,其實只有兩種訪問許可權:private(實現封裝)和其他(不實現封裝)

6. 寧以non-member、non-friend替換member函式

面向物件守則要求,資料以及操作資料的那些函式應該被捆綁在一起,這意味著它建議所有操作資料成員的函式都應該是member函式。然而事實上是如此嗎?

問題產生

假設我們希望寫一個類來描述網頁:

class WebBrowser {
public:
    ...
    void clearCache();
    void clearHistory();
    void removeCookies();
    ...
    // 使用者希望有一個函式能夠清楚所有資訊
    // 問題是,該函式是否應該宣告為member?
    void clearEverything();
};
// 也可以宣告為non-member
void clearEverything(WebBrowser &web) {
...
}

那麼哪種選擇更好呢?

問題解決

根據面向物件守則要求,宣告為member函式應該是更好的選擇。然而,這是對面向物件真實意義的一個誤解。面向物件要求資料應該儘可能被封裝,然而與直觀相反地,member函式clearEverything帶來的封裝性比non-member函式的低。此外,提供non-member函式可允許對WebBrowser相關機能有更大的包裹彈性,從而最終導致較低的編譯相依度,增加WebBrowser的可衍生性。以下我們給出理由。

  • 封裝性。愈多的東西被封裝,越少人可以按到它,那麼我們就有越大的彈性去改變它,而我們的改變只會影響看到改變的那些人和事物。這就是我們推崇封裝性的原因:它使我們能夠改變事物而隻影響有限客戶。
  • 考慮物件內資料。越少程式碼可以看到資料,越多的資料可被封裝,而我們也就越能自動地改變物件資料。越多的函式可以訪問資料成員,資料的封裝性就越差!

因此,因為non-member non-friend函式不能直接改變資料成員,因此他就可以最大限度的實現封裝

解答優化

在C++中,最自然的做法,是讓clearEverything稱為一個non-member函式並且位於WebBrowser所在的同一個namespace內:

namespace WebBrowserStuff {
    class WebBrowser {...};
    void clearEverything(WebBroswer &web);
    ...
}

namespace和class是不用的!前者可以跨越多個原始碼檔案而後者不能,這很重要!

像clearEverything這樣的函式就是便利函式,雖然沒有對WebBrowser有特殊的訪問許可權,但可以極大的便利客戶。而實際上,我們會補充大量的類似的便利函式,並且他們可能分屬於不同的模組,於是我們便採用把不同模組便利函式寫於不同的標頭檔案中,但他們都隸屬於同一個名稱空間:

#include "webbrowser.h" 提供class宣告本身,以及其中核心機能
namespace WebBrowserStuff {
class WebBroser { ... };
    ...  // 核心機能,幾乎所有使用者都需要的non-member便利函式
}

// 標頭檔案 “webbrowserbookmarks.h" 
// 與標籤相關
namespace WebBrowserStuff {
    ... // 與標籤相關的便利函式
}
// 標頭檔案 ”webbrowsercookies.h"
namespace WebBrowserStuff{
    ... // 與cookie相關的便利函式
}
...

注意這是C++標準程式庫的組織方式。標準程式庫中並不是擁有單一、整體、龐大的

7. 若所有引數皆需型別轉換,請為此採用non-member函式

令class支援隱式型別轉換通常是個糟糕的注意。當然也有例外,例如你在建立數值型別時。

問題產生

假設我們需要設計一個有理數類:

class Rational {
public:
    Rational(int numerator = 0, int denominator = 1);
    int numerator() const;
    int denominator() const;
private:
    ...
};
class Rational {
public:
    ...
    const Rational operator*(const Rational& rhs) const;
};

// 於是乎可以輕鬆實現乘法
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth;  //  沒問題
result = result * oneEighth;  //  沒問題

到目前為止還沒有實現致命問題,然而:

result = oneHalf * 2;  // ok!
result = 2 * oneHalf;  // error!
// result = 2.operator*(oneHalf);  of course wrong!

第一個式子能夠成立,是因為實現了隱式型別轉換。編譯器知道你在傳遞一個int,而函式需要的是rational,但它也知道只要呼叫Rational建構函式並賦予你所提供的int,就可以變出一個適當的rational出來,於是就這麼做了。相當於:

const Rational temp(2);
result = oneHalf * temp;

當然這隻涉及non-explicit建構函式,才能這麼做。如果是explicit建構函式,這個語句無法通過編譯。

問題解決

result = oneHalf * 2;  // ok!
result = 2 * oneHalf;  // error!

只有當引數被列於引數列內,這個引數才是隱式型別轉換的合格參與者。地位相當於“被呼叫之成員函式所隸屬的那個物件”-即this物件-那個隱喻引數,絕不是隱式轉換的合格參與者。這就是為什麼語句1能夠通過編譯而語句2不可以。

於是,方法就是,讓operator*稱為一個non-member函式,允許編譯器在每一實參身上執行隱式型別轉換。

const Rational operator*(const Rational& lhs, const Rational& rhs) {
    ...
}
Rational oneFourth(1, 4);
Rational result = oneFourth * 2; // right!
result = 2 * oneFourth; // right!

補充思考:
是否應該把該operator*宣告為friend?

答案是否定的!請注意,member的反面不是friend,而是non-member!在此程式碼中,operator*完全可以藉由rational的public介面完成任務,於是便不必把他宣告為friend。無論何時,如果可以避免friend函式就應該避免。

總結:
如果你需要為某個函式的所有引數(包括this)進行型別轉換,那麼這個函式必須是個non-member。

8. 考慮如何寫出特化的swap函式

swap作為STL的一部分,而後成為異常安全性程式設計的脊柱,以及用來處理自我賦值可能性的一個常見機制。由於此函式如此有用,也意味著他具有非凡哥的複雜度。本節談論這些複雜度以及相應處理。

問題產生1

namespace std {
    template<typename T>
    void swap(T &a, T &b) {
    T temp(a);
    a = b;
    b = temp;
    }
}

這是標準程式庫提供的swap演算法。非常地簡單,只要T有copying相關操作即可。然而這個演算法對於有些情況卻顯得不那麼高效。例如,在處理“以指標指向一個物件,內含真正資料”的那種型別。(這種設計的常見形式是所謂“pimpl手法:pointer to implemention)

class WidgetImpl {  //  實現細節不重要。
public:             //  針對Widget設計的class
    ...
private:
    int a, b, c;
    std::vector<double> v;
    ...
};
class Widget {
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs) {
    ...
    *pImpl = *(rhs.pImpl);
    ...
    }
private:
    WidgetImpl* pImpl;
};

對此類呼叫演算法庫的swap就會非常低效。因為他總共要複製三個Widget和三個WidgetImpl物件!而事實上,只需要改變指標的指向就可以了。

問題解決1

我們可能嘗試用以下方法解決,讓swap針對Widget特化。

嘗試一:

namespace std {
    template<> // 表示他是std::swap的一個全特化
    void swap<Widget>(Widget &a, Widget &b) {
    swap(a.pImpl, b.pImpl);
    }
}

通常來說,我們是不能夠改變std名稱空間內的任何東西,但可以(被允許)為標準template製造特化版本的。

但實際上這個是無法通過編譯的。因為他企圖呼叫class的私有成員。
所以更合理的做法,是令他呼叫成員函式。

解法:

class Widget {
public:
    ...
    void swap(Widget& other) {
    using std::swap;
    swap(pImpl, other.pInmpl);
    }
    ...
};
private:
    WidgetImpl* pImpl;
};

namespace std {
    template<>
    void swap<Widget>(Widget &a,
                        Widget &b) {
        a.swap(b);
    }
}

這個做法不僅能夠通過編譯,而且與STL容器有一致性。

問題產生2

假設Widget和WidgetImpl都是class template而非class,也許我們可以試試把WidgetImpl內的資料型別加以引數化:

template<typename T>
class WidgetImpl {...};
template<typename T>
class Widget {...};
// 在Widget裡面放入swap成員函式就像以往一樣簡單
// 但在寫特化std::swap時出現了問題
namespace std {
    template<typename T>
    void swap< Widget<T> > (Widget<T>& a, Widget<T>& b) {
    a.swap(b);
    }
}

以上特化swap其實有問題的。我們企圖偏特化這個function template,但C++只允許對class template偏特化。(隨後會介紹全特化和偏特化)。當你嘗試偏特化一個function template時,更常見的做法是新增過載函式:

namespace std {
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b) {
    a.swap(b);
    }
}

但實際上,這也是不行的!因為std是個特殊的名稱空間,其管理規則比較特殊,客戶可以全特化std內的template,但不可以新增新的template到std裡面。

問題解決2

解決這個問題的方法就是,宣告一個non-member swap讓它呼叫member swap,但不在將那個non-member swap宣告為std::swap特化版或過載版本。

namespace WidgetStuff {
    template<typename T>
    class WidgetImpl {...};
    template<typename T>
    class Widget {...};
    ...
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b) {
    a.swap(b);
    }
}

現在,任何時候如果打算置換兩個Widget物件,因而呼叫swap,C++的名稱查詢法則都會找到WidgetStuff內的Widget專屬版本。

這個做法對class和class template都行得通。如果你想讓你的”class“專屬版swap在儘可能多的語境下被呼叫,你需要同時在該class所在名稱空間內寫一個non-member版本以及一個std::swap特化版本。

另外,如果沒有像上面那樣額外使用某個名稱空間,上述每件事情仍然使用。但你又何必再global名稱空間裡面塞這麼多東西呢?

補充思考

目前提到得都是和swap編寫有關的。現在我們換位思考,從客戶觀點看看問題。假設我們需要寫一個function template:

template<typename T>
void doSomething(T& obj1, T& obj2) {
...
swap(obj1, obj2);
...
}

此時swap是呼叫哪個版本呢?我們當然希望是呼叫T專屬版本,並且在該版本不存在的情況下,呼叫std內的一般化版本。

template<typename T>
void doSomething(T& obj1, T& obj2) {
using std::swap;
...
swap(obj1, obj2); // 為T型別物件呼叫最佳swap版本。
...
}

C++名稱查詢法則確保將找到global作用域或T所在名稱空間內的任何T專屬的swap。如果T是Widget並位於名稱空間WidgetStuff內,編譯器會使用”實參取決之查詢規則“找出WidgetStuff內的swap。如果沒有T專屬之swap存在,編譯器就使用std內的swap。

以下是我設計的一個不大合乎邏輯的程式碼,但證明了上述說法是合理的。

#include <iostream>
using namespace std;

namespace test {
    class trys {
    public:
        void swap(trys &one, trys &two) {
            cout << "yes!" << endl;
        }
    };
    void swap(trys &one, trys &two) {
        cout << "yes!" << endl;
    }
}
int main(int argc, const char * argv[]) {
    // insert code here...
    test::trys a;
    int b = 12;
    {
        using std::swap;
        swap(b, b);
        swap(a, a);
    }
    return 0;
}
/*
yes!
Program ended with exit code: 0
*/

總結:

如果swap預設實現版的效率不足,(那幾乎意味著你的class或template使用了某種pimpl手法),可以試著做以下事情:

  • 提供一個public swap成員函式,讓他高效地置換你的型別的兩個物件值。
  • 在你的class或template所在的名稱空間內提供一個non-member swap,並命它呼叫上述swap成員函式。
  • 如果你在編寫一個class,併為你的class特化std::swap,並令他呼叫你的swap成員函式。

最後,如果你呼叫swap,請確保包含一個using宣告式。

補充內容:(全特化和偏特化)

模板為什麼要特化,因為編譯器認為,對於特定的型別,如果你能對某一功能更好的實現,那麼就該聽你的。

模板分為類模板與函式模板,特化分為全特化與偏特化。全特化就是限定死模板實現的具體型別,偏特化就是如果這個模板有多個型別,那麼只限定其中的一部分。

先看類模板:

template<typename T1, typename T2>  
class Test  
{  
public:  
    Test(T1 i,T2 j):a(i),b(j){cout<<"模板類"<<endl;}  
private:  
    T1 a;  
    T2 b;  
};  

template<>  
class Test<int , char>  
{  
public:  
    Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}  
private:  
    int a;  
    char b;  
};  

template <typename T2>  
class Test<char, T2>  
{  
public:  
    Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}  
private:  
    char a;  
    T2 b;  
};  

那麼下面3句依次呼叫類模板、全特化與偏特化:

Test<double , double> t1(0.1,0.2);  
Test<int , char> t2(1,'A');  
Test<char, bool> t3('A',true);  

而對於函式模板,卻只有全特化,不能偏特化:

//模板函式  
template<typename T1, typename T2>  
void fun(T1 a , T2 b)  
{  
    cout<<"模板函式"<<endl;  
}  

//全特化  
template<>  
void fun<int ,char >(int a, char b)  
{  
    cout<<"全特化"<<endl;  
}  

//函式不存在偏特化:下面的程式碼是錯誤的  
/* 
template<typename T2> 
void fun<char,T2>(char a, T2 b) 
{ 
    cout<<"偏特化"<<endl; 
} 
*/  

至於為什麼函式不能偏特化,似乎不是因為語言實現不了,而是因為偏特化的功能可以通過函式的過載完成。
(摘自:模板的全特化與偏特化