Item 11: 比起private undefined function優先使用deleted function

本文翻譯自《effective modern C++》,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝!

博客已經遷移到這裏啦

如果你爲其他開發者提供代碼,並且你想阻止他們調用一個特定的函數,你通常不會聲明這個函數。函數不聲明,函數就不會被調用。太簡單了!但是有時候C++會幫你聲明函數,並且如果你想要阻止客戶調用這些函數,簡單的事情就不再簡單了。

這種情況只發生在“特殊的成員函數”身上,也就是,當你需要這些成員函數的時候,C++會自動幫你生成。Item 17詳細地討論了這些函數,但是現在,我們只考慮copy構造函數和copy assignment operator。這章主要講的是,用C++11中更好的做法替換在C++98中的常用做法。然後在C++98中,你最想抑制的成員函數,常常是copy構造函數,assignment operator,或都想抑制。

在C++98中,阻止這些函數的方法是:把它們聲明成private的,並且不去定義它們。舉個例子,C++標準庫中的iostream類層次的底層有個class template叫做basic_ios。所有istream和ostream繼承自(可能不是直接地)這個類。拷貝istream和ostream是不受歡迎的,因爲沒有一個清晰的概念規定這些操作應該做些什麼。舉個例子,一個istream對象表示一些輸入值的流,有些值已經被讀過了,有些值可能會在之後讀入。如果一個istream被拷貝,那麼是否有必要拷貝所有之前讀過以及以後要讀的值呢?處理這個問題的最簡單的辦法就是,定義它們爲不存在的。要做到這點,只需要禁止stream的拷貝就行了。

爲了使istream和ostream類不能拷貝,basic_ios在C++98中如此實現(包括註釋):

template<class charT, class traits = char_traits<charT> >
class basic_ios : public ios_base{
public:
    ...

public:
    basic_ios(const basic_ios&);            // not defined
    basic_ios& operator=(const basic_ios&); //not defined   
};

把這些函數聲明爲private,可以阻止客戶調用它們。故意不去定義它們意味着如果一些代碼有權利訪問它們(也就是,成員函數或友元類),並使用它們,那麼在鏈接的時候,就會因爲找不到函數定義而失敗。

在C++11中,有更好的辦法,它能在本質上實現所需的功能:使用“=delete”來標記copy 構造函數和copy assignment operator,讓它們成爲deleted函數。這裏給出C++11中的basic_ios的實現:

template<calss charT, class trais = char_traits<charT> >
class basi_ios : public ios_base{
public:
    ...
    basic_ios(const basic_ios&) = delete;
    basic_ios& operator= (const basic_ios&) = delete;
    ...
};

把這些函數“刪除掉”和把它們聲明爲private的不同之處看起來除了更時尚一點就沒別的了,但是這裏有一些實質上的優點是你沒想到的。deleted 函數不會被任何方式使用,所以就算在成員函數和友元函數中,它們如果嘗試拷貝basic_ios對象,它們也會失敗。比起C++98(這樣的錯誤使用在鏈接前無法被診斷出來),這算是一個提升。

按照慣例,deleted函數被聲明爲public,而不是private。這是有原因的。當客戶代碼嘗試使用一個成員函數,C++在檢查deleted狀態之前,會先檢查它的可訪問性。當客戶代碼嘗試使用一個deleted private函數,儘管函數的可訪問性不會影響到它能否被使用(這個函數總是不可調用的),一些編譯器只會“抱怨”出函數是private的。當修改歷史遺留的代碼,把private-and-not-defined成員函數替換成deleted函數時,尤其要記得這一點(聲明deleted函數爲public的),因爲讓新函數成爲public的,將產生更好的錯誤消息。

比起必須要把函數聲明爲private的,deleted函數還有一個關鍵的優點,那就是任何函數都可以成爲deleted的。舉個例子,假設我們有一個非成員函數,這個函數以一個整形爲參數,並且返回一個bool表示它是否是幸運數字:

bool isLucky(int number);

C++是從C繼承來的,這意味着很多其它類型能被模糊地視爲數值類型,然後隱式轉換到int,但是一些能通過編譯的調用是沒有意義的:

if(isLucky('a')) ...            //'a'是一個幸運數字嗎?

if(isLucky(true))...            //"true"是幸運數字嗎?

if(isLucky(3.5))...             //在檢查它的幸運屬性前,我們是否應該
                                //把它截斷爲3

如果幸運數字必須是整形類型,我們可以阻止上面這些調用。

一種方式是用我們想過濾掉的類型創建deleted重載:

bool isLucky(int number);

isLucky(char) = delete;         //拒絕char

isLucky(bool) =  delete;        //拒絕bool

isLucky(double) = delete;       //拒絕double和float

(你可能會感到奇怪:double重載版本的註釋中說double和float都被拒絕了。只要你記起:給出從float到int以及float到double的轉換時,C++會更優先把float轉換到double,你的疑問就消散了。因此,用float調用isLucky會調用double版本而不是int版本的重載。好了,它(編譯器)會先嚐試調用isLucky,事實上這個版本的重載是deleted的,所以在編譯時就會阻止這個調用。)

儘管deleted函數不能被使用,它們還是你程序中的一部分。因此,它們在重載解析時,它們會被考慮進去。這就是爲什麼只要使用上面這樣的deleted函數聲明式,令人討厭的調用就被拒絕了:

if(isLucky('a')) ...            //錯誤,調用一個deleted函數

if(isLucky(true))...            //錯誤

if(isLucky(3.5))...             //錯誤

deleted函數還有一個使用技巧(private 成員函數做不到),那就是阻止不需要的template實例。舉個例子,假設你需要一個使用built-in指針的template(第四章的建議是,比起raw指針,優先使用智能指針):

template<typename T>
void processPointer(T* ptr);

在指針的世界中,有兩種特殊的情況。一種是void*指針,因爲他們無法解引用,無法增加或減少,等等。另外一個就是char*指針,因爲他們常用來代表指向C風格字符串的指針,而不是指向單個字符的指針。這些特殊的情況常常需要特別處理。現在,在processPointer template中,讓我們假設我們需要做的特殊處理是拒絕這些類型的調用。也就是不能使用void*char*指針來調用processPointer。

這很容易執行,只要把他們的實例刪除(delete)掉:

template<>
void processPointer<void>(void*) = delete;

template<>
void processPointer<char>(char*) = delete;

現在,我們用void*或者char*調用processPointer是無效的,const void*const char*可能也需要是無效的,因此,這些實例也需要被刪除(delete):

template<>
void processPointer<const void>(const void*) = delete;

template<>
void processPointer<const char>(const char*) = delete;

並且你真想做的很徹底的話,你還需要刪除(delete)掉const volatile void*const volatile char*重載,然後你需要再爲其他標準字符類型(std::wchar_t, std::char16_t以及std::char32_t)做這樣的工作。

有意思的是,如果你有一個函數template內嵌於一個class,然後你想通過把特定的實例聲明爲private(啊啦,典型的C++98的方法)來使它們無效,這是無法實現的,因爲你無法把一個成員函數template特化爲不同的訪問等級(和主template的訪問等級不同)。舉個例子,如果processPointer是一個內嵌於Widget的成員函數template,然後你想讓void*指針的調用失效,儘管無法通過編譯,C++98的方法看起來像是這樣:

class Widget{
public:
    ...
    template<typename T>
    void processPointer(T* ptr)
    { ... }

private:
    template<>
    void processPointer<void>(void*);   //錯誤
};

問題在於template特化必須寫在命名空間的作用域中,而不是類的作用域中。這個問題不會影響deleted函數,因爲他們不需要不同的訪問等級。他們能在class外面被刪除(因此處在命名空間的作用域中):

class Widget{
public:
    ...
    template<typename T>
    void processPointer(T* ptr)
    { ... }

    ...
};  

template<>
void Widget::processPointer<void>(void*) = delete;

事實上,C++98中,聲明函數爲private並不定義它們就是在嘗試實現C++11的delted函數所實現的東西。作爲一個模仿,C++98的方法沒有做到和實物(C++11的deleted函數)一模一樣。它在class外面無法工作,它在class裏面不總是起作用,就算它起作用,它在鏈接前可能不起作用。所以堅持使用deleted函數把!!!

            你要記住的事
  • 比起private undefined function優先使用deleted function
  • 任何函數都能被刪除(deleted),包括非成員函數和template實例化函數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章