Effective Modern C++: Item 11 -> 优先选择deleted函数而不是私有未定义函数

来源:互联网 发布:照片视频制作软件 编辑:程序博客网 时间:2024/06/05 02:01

优先选择deleted函数而不是私有未定义函数

如果你要提供代码给其他的开发者,而你想要阻止他们调用某个特定函数,一般来说你就不声明这个函数了。没有函数声明也就没有函数可调用。简单,小菜一碟!但有时候C++会自动给你声明函数,而如果你想要阻止客户调用这些函数,那就不是小菜一碟了。

这种情况只有在”特殊成员函数”那里才会出现,特殊成员函数就是当它们需要时,C++会自动给你生成的成员函数。Item 17具体讨论了这些函数,但是现在,我们只考虑复制构造函数和复制赋值操作符。这一章主要致力于讲C++98中那些被C++11中更好的做法替代的常见做法。而在C++98中,如果你想要抑制某些成员函数的使用,这几乎总是赋值构造函数或者赋值操作符,亦或者两个都是。

C++98中阻止这些函数被使用的方式是通过将它们定义成私有的(private)并且不定义它们。例如,C++标准库中iostreams层次中靠近基类的是类模板basic_ios。所有的istream和ostream都继承(可能是间接地)自这个类。复制istream和ostream是不可取的,因为并不清楚这类操作应该怎么做。例如,一个istream对象表示一个输入流,其中有一些可能已经读取进来了,而另一些可能要过一会才读进来。如果对一个istream对象进行复制,这是不是需要将目前已经读取进来的所有的值进行复制,另外还包括后面即将读取进来的值?最简单的处理这种问题的方式就是不定义它们。禁止拷贝stream就这样做的。

为了让istream和ostream类不可拷贝,C++98中的basic_ios*是这样实现的(包括评论):

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

将这些函数声明成私有的可以防止客户调用它们。故意不实现它们意味着如果能够访问它们的代码(也就是说,该类的成员函数或者友元)使用了它们,链接就会因为缺失函数定义而报错。

在C++11中,有一种更好的方法能够实现相同的目的:使用”= delete”来标记复制构造函数和赋值操作符,使其成为deleted函数。下面是C++11中实现的相同部分的basic_ios:

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

看起来删除这些函数和将它们声明成私有的方式之间的区别只是一种风格上的,但是这里面的区别比你可能认为的要大。deleted函数不管以什么方式都不能被使用,所以即使是成员函数或者友元函数里面的代码尝试复制basic_ios对象也会编译报错。这相比于C++98是一个进步,因为在C++98里,这种错误得要到链接阶段才能诊断出来。

按照惯例,deleted函数一般声明为公开的,而不是私有的,这里面室友原因的。当客户代码尝试使用成员函数时,C++在检查删除状态之前会首先检查可访问性。当客户代码尝试使用deleted private函数时,一些编译器只会提示说函数是私有的,尽管该函数的可访问性并不真的影响它是否可用。当你修改遗留代码,使用deleted函数来替换那些私有未定义函数时,请牢记上面所说的,因为让deleted函数公开可以让编译器产生更好的错误信息。

deleted函数一个很重要的优势就是任何函数都可以被删除,而只有成员函数可以私有化。例如,假设我们有一个非成员函数,接受一个整型并返回它是否是幸运数:

bool isLucky(int number);

C++的C基因意味着差不多任何只要能模糊地被看做为数值的类型都会被隐性转换成int,但是一些可以通过编译的调用可能并没有意义:

if (isLucky('a')) ...  //is 'a' a lucky number?if(isLukcy(true))...   //is "true"?if(isLucky(3.5))...    //should we truncate to 3 before                        //checking for luckiness

如果幸运数必须真的是一个整型,我们希望能够防止像上面的调用通过编译。

一种方法就是对于我们希望过滤掉的类型创建其deleted重载:

bool isLucky(int number);   //original functionbool isLucky(char) = delete;    //reject charsbool isLucky(bool) = delete;    //reject boolsbool isLucky(double) = delete;  //reject doubles and floats

(double版本的重载边上的评论说double和float都会被拒绝,这可能会使你感到惊讶,但是你的惊讶会立刻烟消云散,只要你会想起这个:如果在float转int和float转double之间做出选择,C++更青睐转double。使用float值调用isLucky因此会调用double那个版本,而不是int那个版本。好吧,它只是尝试去。重载函数被删除的事实将阻止对其的调用通过编译)

尽管deleted函数不能被使用,它们却是你程序中的一部分。也因此,在重载决议过程中,它们也会被纳入考虑。这也就是为什么,上面加入了deleted函数声明,不希望的isLucky调用会被拒绝:

if (isLucky('a')) ...  //error! call to deleted functionif(isLukcy(true))...   //error!if(isLucky(3.5f))...    //error!

另一个deleted函数可以展示的把戏(而私有的成员函数则不可以)就是阻止使用被禁用的模板实例。例如,假设你需要一个和内置指针打交道的模板(尽管第4章建议优先选择智能指针而不是原始指针):

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

在指针世界里存在两个特殊的case。一个就是void*指针,因为它没法被解引用,也没法增加或者减少等。另一个就是char*指针了,因为它们一般表示指向C风格的字符串的指针,而不是指向单独的字符。这些特殊的case经常需要特殊的处理,而在我们的processPointer这个case里,假设这种处理方式就是拒绝使用这些类型的调用。那就是说,不能够使用void*和char*来调用processPointer。

这很容易实施。只需要删除掉那些实例:

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

现在,如果用void*和char*调用是不合法的,那么很可能用const void*和const char*调用也是不合法的,所以这些类型的实例也应该被删除:

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

如果你真的想非常彻底,那么你也应该删除掉const volatile void*和const volatite char*的重载,然后你就可以使用指向其他标准字符类型诸如std::wchar_t,std::char16_t和std::char32_t的指针的重载版本了。

有趣的是,如果你在一个类内部有一个函数模板,而你想通过将某些模板实例声明成私有的来达到禁止的目的(经典的C++98传统),但是你不能,因为你不能给一个成员函数模板全特化一个和主模板不同的访问级别。例如,如果processPointer是一个Widget内部的成员函数模板,而你想要禁止void*指针的调用,下面将是C++98风格的方法,尽管它通不过编译:

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

问题就在于模板全特化必须写在命名空间范围内,而不是在类范围内。这种问题不会出现在deleted函数身上,因为它们并不需要一个不同的访问级别。它们可以在类外面被删除(所以依旧处于命名空间范围):

public:    ...    template<typename T>    void processPointer(T *ptr)    {...}};template<>void processPointer<void>(void*) = delete;//still public,                                          //but deleted

事实就是C++98那种将函数定义成私有的而且不实现它们实际上就是想实现C++11里面deleted函数所实现的功能。作为仿真,C++98那种方法其实不如真的好。它不能在类外面工作,它在类里面也不总是能工作,并且当它可以工作时,它也可能在链接的时候又不工作了。所以还是坚持deleted函数吧。

要点记忆

  • 优先选择deleted函数而不是私有未定义函数
  • 任何函数都可以被删除,包括非成员函数和模板实例
阅读全文
0 0