EffectiveModernCppChinese/3.MovingToModernCpp/item11.md
2018-06-05 20:07:47 +08:00

7.2 KiB
Raw Blame History

Item 11:优先考虑使用deleted函数而非使用未定义的私有声明

条款11:优先考虑使用deleted函数而非使用未定义的私有声明

如果你写的代码要被其他人使用你不想让他们调用某个特殊的函数你通常不会声明这个函数。无声明不函数。简简单单但有时C++会给你自动声明一些函数,如果你想防止客户调用这些函数,事情就不那么简单了。

上述场景见于特殊的成员函数即当有必要时C++自动生成的那些函数。Item 17 详细讨论了这些函数但是现在我们只关心拷贝构造函数和拷贝赋值运算符重载。This chapter is largely devoted to common practices in C++98 that have been superseded by better practices in C++11, and in C++98, if you want to suppress use of a member function, its almost always the copy constructor, the assignment operator, or both.

在C++98中防止调用这些函数的方法是将它们声明为私有成员函数。举个例子在C++ 标准库iostream继承链的顶部是模板类basic_ios。所有istreamostream类都继承此类(直接或者间接)。拷贝istreamostream是不合适的,因为要进行哪些操作是模棱两可的。比如一个istream对象,代表一个输入值的流,流中有一些已经被读取,有一些可能马上要被读取。如果一个istream被拷贝,需要像拷贝将要被读取的值那样也拷贝已经被读取的值吗?解决这个问题最好的方法是不定义这个操作。直接禁止拷贝流。

要使istreamostream类不可拷贝,basic_ios在C++98中是这样声明的(包括注释)

template <class charT, class traits = char_traits<charT> >
class basic_ios : public ios_base {
public:
    
private:
    basic_ios(const basic_ios& ); // not defined
    basic_ios& operator=(const basic_ios&); // not defined
};

将它们声明为私有成员可以防止客户端调用这些函数。故意不定义它们意味着假如还是有代码用它们就会在链接时引发缺少函数定义(missing function definitions)错误。

在C++11中有一种更好的方式只需要使用相同的结尾= delete将拷贝构造函数和拷贝赋值运算符标记为deleted函数。上面相同的代码在C++11中是这样声明的

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;
    
};

删除这些函数(译注:添加"= delete")和声明为私有成员可能看起来只是方式不同,别无其他区别。其实还有一些实质性意义。deleted函数不能以任何方式被调用,即使你在成员函数或者友元函数里面调用deleted函数也不能通过编译。这是较之C++98行为的一个改进后者不正确的使用这些函数在链接时才被诊断出来。

通常,deleted函数被声明为public而不是private.这也是有原因的。当客户端代码试图调用成员函数C++会在检查deleted状态前检查它的访问性。当客户端代码调用一个私有的deleted函数,一些编译器只会给出该函数是private的错误(译注:而没有诸如该函数被deleted修饰的错误),即使函数的访问性不影响它的使用。所以值得牢记,如果要将老代码的"私有且未定义"函数替换为deleted函数时请一并修改它的访问性为public,这样可以让编译器产生更好的错误信息。

deleted函数还有一个重要的优势是任何函数都可以标记为deleted,而只有private只能修饰成员函数。假如我们有一个非成员函数,它接受一个整型参数,检查它是否为幸运数:

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); // 原始版本
bool isLucky(char) = delete; // 拒绝char
bool isLucky(bool) = delete; // 拒绝bool
bool isLucky(double) = delete; // 拒绝float和double

(上面double重载版本的注释说拒绝float和double可能会让你惊讶但是请回想一下float转换为intdoubleC++更喜欢转换为double。使用float调用isLucky因此会调用double重载版本,而不是int版本。好吧,它也会那么去尝试。事实是调用被删除的double重载版本不能通过编译。不再惊讶了吧。)

虽然deleted寒暑假不能被使用,它它们还是存在于你的程序中。也即是说,重载决议会考虑它们。这也是为什么上面的函数声明导致编译器拒绝一些不合适的函数调用。

if (isLucky('a'))  //错误! 调用deleted函数
if (isLucky(true))  // 错误!
if (isLucky(3.5f))  // 错误!

另一个deleted函数用武之地private成员函数做不到的地方是禁止一些模板的实例化。 假如你要求一个模板仅支持原生指针(尽管第四章建议使用智能指针代替原生指针)

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

在指针的世界里有两种特殊情况。一是void*指针,因为没办法对它们进行解引用,或者加加减减等。 另一种指针是char*因为它们通常代表C风格的字符串而不是正常意义下指向单个字符的指针。 这两种情况要特殊处理,在processPointer模板里面,我们假设正确的函数应该拒绝这些类型。 也即是说,processPointer不能被void*char*调用。 要想确保这个很容易,使用delete标注模板实例:

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

现在如果使用void*char*调用processPointer就是无效的,按常理说const void*const void*也应该无效,所以这些实例也应该标注delete:

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

And if you really want to be thorough, youll also delete the const volatile void* and const volatile char* overloads, and then youll get to work on the overloads for pointers to the other standard character types: std::wchar_t, std::char16_t, and std::char32_t.

Interestingly, if you have a function template inside a class, and youd like to disable some instantiations by declaring them private (à la classic C++98 convention), you cant, because its not possible to give a member function template specialization a different access level from that of the main template. If processPointer were a member function template inside Widget, for example, and you wanted to disable calls for void* pointers, this would be the C++98 approach, though it would not compile: