11.以deleted函数取代private undefined函数

前言

 
在C++98中我们倾向于使用privtae undefined来禁止用户使用某个函数(例如copy constructor或者operator =),C++11引入了新的解决方案。


问题实例

 
在C++98中,为了保证istream与ostream无法被拷贝,basic_ios有定义如下:

1
2
3
4
5
6
7
8
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
};

这么做的原因可见Effective C++ Item6。

在C++11中,我们可以使用=delete来更好地完成这一任务:

1
2
3
4
5
6
7
8
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;

};

C++11中的deleted function将问题发现的时间提前到了编译期,即任何试图调用函数函数的操作都将引发编译期错误,但private undefined function则可能由于友元调用等行为在连接期才会发现函数未定义从而报错(解决方法是使用mixin class uncopyable将报错时机提前至编译期)。


delete function的访问类型

 
我们倾向于将delete function设置为public,因为发生函数调用时编译首先会检查其访问类型,然后再判断其delete status。如果用户试图调用一个deleted private function,某些编译器可能会本末倒置地提示该函数不可访问,而非该函数不可使用。在修改历史遗留代码时应当考虑这一点,这样才能保证编译器的错误提示正确与直观。


delete function的优势

任何函数均可delete

delete function的一大优点在于任何函数均可设为delete function,而只有成员函数才能被设为private。举例而言,假若我们有一个非成员函数,其接受一个int且返回一个bool:

1
bool isLucky(int number);

C++继承自C的特性使得可能某些对象可能会被隐性地转为int从而触发该函数:
1
2
3
if (isLucky('a')) … // is 'a' a lucky number?
if (isLucky(true)) … // is "true"?
if (isLucky(3.5)) … // should we truncate to 3 before checking for luckiness?

如果我们需要isLucky仅仅针对int有效,那我们可以利用重载与delete function将其余函数定义为不可使用:

1
2
3
4
bool isLucky(int number); // original function
bool isLucky(char) = delete; // reject chars
bool isLucky(bool) = delete; // reject bools
bool isLucky(double) = delete; // reject doubles and floats

需要注意的是最后一个重载可以同时阻止double与float发生隐式转换,因为相较于int,C++编译器更倾向于将float隐式转换为double。


禁止模板实例化

delete function相较于private undefined function较优的另一点在于其可以禁止模板实例化。举例而言,假设当前有一个形参为内置指针的模板函数(尽管本书第4章建议使用智能指针):

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

众所周知,指针类型有两种特殊情况:void* 与 char*。前者无法被解引用,也无法执行算数操作。后者则通常作为字符串的象征。我们认为processPointer函数模板需要对这两种指针进行特殊处理,即不针对这两种类型实例化函数,通过delete我们可以轻松实现所述功能:
1
2
3
4
template<>
void processPointer<void>(void*) = delete;
template<>
void processPointer<char>(char*) = delete;

现在并没有万事大吉,因为如果void*与char*无法实例化模板,那么const void*与const char*理当也无法实例化模板才对,因此我们不辞劳苦地再写一次:
1
2
3
4
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,and std::char32_t之类的东西都定义为无效。

在C++98当中,如果某个类内含一个函数模板,那我们无法采用private undefined的形式对其进行禁止实例化。原因很简单,不可能针对某个特例化改变整个函数模板的访问级别,如下行为将无法通过编译:

1
2
3
4
5
6
7
8
9
10
class Widget {
public:

template<typename T>
void processPointer(T* ptr)
{ … }
private:
template<> // error!
void processPointer<void>(void*);
};

模板特例化理当出现在某个命名空间内,而非处于类范围内。使用delete function则无此烦恼,因为它们不需要不同的访问级别,并且可以处于类外被deleted:
1
2
3
4
5
6
7
8
9
10
class Widget {
public:

template<typename T>
void processPointer(T* ptr)
{ … }

};
template<> // still
void Widget::processPointer<void>(void*) = delete; // public,but deleted

我们可以认为private undefined function是deleted function尚未出现时C++98的权宜之计,我们应当尽可能使用deleted function将其替换掉。


总结

  1. 尽可能以deleted function取代private undefined function。
  2. 任何function均可被delete,其中包括非成员函数与模板实例化。