條款11:優先使用delete關鍵字刪除函數而不是private卻又不實現的函數
=========================
如果你要給其他開發者提供代碼,并且還不想讓他們調用特定的函數,你只需要不聲明這個函數就可以了。沒有函數聲明,沒有就沒有函數可以調用。這是沒有問題的。但是有時候`C++`為你聲明了一些函數,如果你想阻止客戶調用這些函數,就不是那么容易的事了。
這種情況只有對“特殊的成員函數”才會出現,即這個成員函數是需要的時候`C++`自動生成的。條款17詳細地討論了這種函數,但是在這里,我們僅僅考慮復制構造函數和復制賦值操作子。這一節致力于`C++98`中的一般情況,這些情況可能在`C++11`中已經不復存在。在`C++98`中,如果你想壓制一個成員函數的使用,這個成員函數通常是復制構造函數,賦值操作子,或者它們兩者都包括。
在`C++98`中阻止這類函數被使用的方法是將這些函數聲明為`private`,并且不定義它們。例如,在`C++`標準庫中,`IO`流的基礎是類模板`basic_ios`。所有的輸入流和輸出流都繼承(有可能間接地)與這個類。拷貝輸入和輸出流是不被期望的,因為不知道應該采取何種行為。比如,一個`istream`對象,表示一系列輸入數值的流,一些已經被讀入內存,有些可能后續被讀入。如果一個輸入流被復制,是不是應該將已經讀入的數據和將來要讀入的數據都復制一下呢?處理這類問題最簡單的方法是定義這類問題不存在,`IO`流的復制就是這么做的。
為了使`istream`和`ostream`類不能被復制,`basic_ios`在`C++98`中是如下定義的(包括注釋):
```cpp
template <class charT, class traits = char_traits<charT> >
class basic_ios :public ios_base {
public:
...
private:
basic_ios(const basic_ios& ); // 沒有定義
basic_ios& operator(const basic_ios&); // 沒有定義
};
```
將這些函數聲明為私有來阻止客戶調用他們。故意不定義它們是因為,如果有函數訪問這些函數(通過成員函數或者友好類)在鏈接的時候會導致沒有定義而觸發的錯誤。
在`C++11`中,有一個更好的方法可以基本上實現同樣的功能:用`= delete`標識拷貝復制函數和拷貝賦值函數為刪除的函數`deleted functions`。在`C++11`中`basic_ios`被定義為:
```cpp
template <class charT, class traits = char_traits<charT> >
class basic_ios : public ios_base {
public:
...
basic_ios(const basic_ios& ) = delete;
basic_ios& operator=(const basic_ios&) = delete;
...
};
```
刪除的函數和聲明為私有函數的區別看上去只是時尚一些,但是區別比你想象的要多。刪除的函數不能通過任何方式被使用,即便是其他成員函數或者友好函數試圖復制`basic_ios`對象的時候也會導致編譯失敗。這是對`C++98`中的行為的升級,因為在`C++98`中直到鏈接的時候才會診斷出這個錯誤。
方便起見,刪除函數被聲明為公有的,而不是私有的。這樣設計的原因是,當客戶端程序嘗試使用一個成員函數的時候,`C++`會在檢查刪除狀態之前檢查可訪問權限。當客戶端代碼嘗試訪問一個刪除的私有函數時,一些編譯器僅僅會警報該函數為私有,盡管這里函數的可訪問性并不本質上影響它是否可以被使用。當把私有未定義的函數改為對應的刪除函數時,牢記這一點是很有意義的,因為使這個函數為公有的可以產生更易讀的錯誤信息。
刪除函數一個重要的優勢是任何函數都可以是刪除的,然而僅有成員函數才可以是私有的。舉個例子,加入我們有個非成員函數,以一個整數位參數,然后返回這個參數是不是幸運數字:
```cpp
bool isLucky(int number);
```
`C++`繼承于`C`意味著,很多其他類型被隱式的轉換為`int`類型,但是有些調用可以編譯但是沒有任何意義:
```cpp
if(isLucky('a')) ... // a 是否是幸運數字?
if(isLucky(ture)) ... // 返回true?
if(isLucky(3.5)) ... // 我們是否應該在檢查它是否幸運之前裁剪為3?
```
如果幸運數字一定要是一個整數,我們希望能到阻止上面那種形式的調用。
完成這個任務的一個方法是為想被排除出去的類型的重載函數聲明為刪除的:
```cpp
bool isLucky(int number); // 原本的函數
bool isLucky(char) = delete; // 拒絕char類型
bool isLucky(bool) = delete; // 拒絕bool類型
bool isLucky(double) = delete; // 拒絕double和float類型
```
(對`double`的重載的注釋寫到:`double`和`float`類型都講被拒絕可能會令你感到吃驚,當時當你回想起來,如果給`float`一個轉換為`int`或者`double`的可能性,`C++`總是傾向于轉化為`double`的,就不會感到奇怪了。以`float`類型調用`isLucky`總是調用對應的`double`重載,而不是`int`類型的那個重載。結果就是將`double`類型的重載刪除將會組織`float`類型的調用編譯。)
盡管刪除函數不能被使用,但是它們仍然是你程序的一部分。因此,在重載解析的時候仍會將它們考慮進去。這也就是為什么有了上面的那些聲明,對`isLucky`不被期望的調用會被拒絕:
```cpp
if (isLucky('a')) ... // 錯誤!調用刪除函數
if (isLucky(true)) ... // 錯誤!
if (isLucky(3.5f)) ... // 錯誤!
```
還有一個刪除函數可以完成技巧(而私有成員函數無法完成)是可以阻止那些應該被禁用的模板實現。舉個例子,假設你需要使用一個內嵌指針的模板(雖然第4章建議使用智能指針而不是原始的指針):
```cpp
template<typename T>
void processPointer(T* ptr);
```
在指針的家族中,有兩個特殊的指針。一個是`void*`指針,因為沒有辦法對它們解引用,遞增或者遞減它們等操作。另一個是`char*`指針,因為它們往往表示指向`C`類型的字符串,而不是指向獨立字符的指針。這些特殊情況經常需要特殊處理,在`processPointer`模板中,假設對這些特殊的指針合適的處理方式拒絕調用。也就是說,不可能以`void*`或者`char*`為參數調用`processPointer`。
這是很容易強迫實現的。僅僅需要刪除這些實現:
```cpp
template<>
void processPointer<void>(void*) = delete;
template<>
void processPointer<char>(char*) = delete;
```
現在,使用`void*`或者`char*`調用`processPointer`是無效的,使用`const void*`或者`const char*`調用也需要是無效的,因此這些實現也需要被刪除:
```cpp
template<>
void processPointer<const void>(const void*) = delete;
template<>
void processPointer<const char>(const char*) = delete;
```
如果你想更徹底一點,你還要刪除對`const volatile void*`和`const volatile char*`的重載,你就可以在其他標準的字符類型的指針`std::wchar_t, std::char16_t`和`std::char32_t`上愉快的工作了。
有趣的是,如果你在一個類內部有一個函數模板,你想通過聲明它們為私有來禁止某些實現,但是你通過這種方式做不到,因為賦予一個成員函數模板的某種特殊情況下擁有不同于模板主體的訪問權限是不可能。舉個例子,如果`processPointer`是`Widget`內部的一個成員函數模板,你想禁止使用`void*`指針的調用,下面是一個`C++98`風格的方法,下面代碼依然無法通過編譯:
```cpp
class Widget{
public:
...
template<typename T>
void processPointer(T* ptr)
{ ... }
private:
template<> // 錯誤!
void processPointer<void>(void*)
};
```
這里的問題是,模板的特殊情況必須要寫在命名空間的作用域內,而不是類的作用域內。這個問題對于刪除函數是不存在的,因為它們不再需要一個不同的訪問權限。它們可以再類的外面被聲明為是被刪除的(也就是在命名空間的作用域內):
```cpp
class Widget{
public:
...
template<typename T>
void processPointer(T* ptr)
{ ... }
...
};
template<> // 仍然是公用的,但是已被刪除
void Widget::processPointer<void>(void*) = delete;
```
真相是,`C++98`中聲明私有函數但是不定義是想達到`C++11`中刪除函數同樣效果的嘗試。作為一個模仿品,`C++98`的方式并不如它要模仿的東西那么好。它在類的外邊和內部都是是無法工作的,當它工作時,知道鏈接的時候可能又不工作了。所以還是堅持使用刪除函數吧。
|要記住的東西|
|:--------- |
|優先使用刪除函數而不是私有而不定義的函數|
|任何函數都可以被聲明為刪除,包括非成員函數和模板實現|
- 出版者的忠告
- 致謝
- 簡介
- 第一章 類型推導
- 條款1:理解模板類型推導
- 條款2:理解auto類型推導
- 條款3:理解decltype
- 條款4:知道如何查看類型推導
- 第二章 auto關鍵字
- 條款5:優先使用auto而非顯式類型聲明
- 條款6:當auto推導出非預期類型時應當使用顯式的類型初始化
- 第三章 使用現代C++
- 條款7:創建對象時區分()和{}
- 條款8:優先使用nullptr而不是0或者NULL
- 條款9:優先使用聲明別名而不是typedef
- 條款10:優先使用作用域限制的enmu而不是無作用域的enum
- 條款11:優先使用delete關鍵字刪除函數而不是private卻又不實現的函數
- 條款12:使用override關鍵字聲明覆蓋的函數
- 條款13:優先使用const_iterator而不是iterator
- 條款14:使用noexcept修飾不想拋出異常的函數
- 條款15:盡可能的使用constexpr
- 條款16:保證const成員函數線程安全
- 條款17:理解特殊成員函數的生成
- 第四章 智能指針
- 條款18:使用std::unique_ptr管理獨占資源
- 條款19:使用std::shared_ptr管理共享資源
- 條款20:在std::shared_ptr類似指針可以懸掛時使用std::weak_ptr
- 條款21:優先使用std::make_unique和std::make_shared而不是直接使用new
- 條款22:當使用Pimpl的時候在實現文件中定義特殊的成員函數
- 第五章 右值引用、移動語義和完美轉發
- 條款23:理解std::move和std::forward
- 條款24:區分通用引用和右值引用
- 條款25:在右值引用上使用std::move 在通用引用上使用std::forward
- 條款26:避免在通用引用上重定義函數
- 條款27:熟悉通用引用上重定義函數的其他選擇
- 條款28:理解引用折疊
- 條款29:假定移動操作不存在,不廉價,不使用
- 條款30:熟悉完美轉發和失敗的情況
- 第六章 Lambda表達式
- 條款31:避免默認的參數捕捉
- 條款32:使用init捕捉來移動對象到閉包
- 條款33:在auto&&參數上使用decltype當std::forward auto&&參數
- 條款34:優先使用lambda而不是std::bind
- 第七章 并發API
- 條款35:優先使用task-based而不是thread-based
- 條款36:當異步是必要的時聲明std::launch::async
- 條款37:使得std::thread在所有的路徑下無法join
- 條款38:注意線程句柄析構的行為
- 條款39:考慮在一次性事件通信上void的特性
- 條款40:在并發時使用std::atomic 在特殊內存上使用volatile
- 第八章 改進
- 條款41:考慮對拷貝參數按值傳遞移動廉價,那就盡量拷貝
- 條款42:考慮使用emplace代替insert