附录B 弃用的特性
随着C++11的发布和新特性的出现,一些C++98与C++03中的特性也即将被淘汰。其中一些被更强大的新特性所取代,如auto_ptr等,也有因为各种缺陷而在实际编程中很少被使用的,如export、register等。相比于不兼容性,了解为什么弃用往往更能了解语言在如何发展。本章将详细描述并总结被C++11所弃用的各种特性。
条目1 auto关键字
旧特性:auto用来标识具有自动存储期的局部变量。
改动:auto关键字可以用来从变量的初始值中推导出变量的类型。
在旧标准中,auto用来声明具有自动存储期的局部变量,这样变量就是自动存储类别,属于动态存储方式,当变量离开作用域后存储空间会被自动释放。实际上,所有非静态的局部变量默认都具有自动的存储期,因此auto关键字很少被用到。
在C++11新标准中,auto被作为一个新的类型修饰符,声明变量时不用指定变量类型,而是根据该变量的初始化表达式或者一个具有追踪返回类型的函数定义推导得来。下面举一个例子,见以下代码:
for(vector<int>::iterator i=vec.begin();i<vec.end();i++){
vector<int>::iterator j=i;
}
在C++11中,我们可以使用auto关键字来提高可读性。
for(auto i=vec.begin();i<vec.end();i++){
auto j=i;
}
此外,auto类型推导使用非常灵活,它几乎可以用在任何需要声明变量类型的上下文中,比如命名空间、for循环的循环变量初始化及for循环体,以及判断语句中,甚至能被使用在模板中。但是,auto不可以声明函数参数,也不能推导数组类型。
由于auto在C++11中被赋予了新的语义,为了避免混淆,C++11中auto不再作为存储的变量声明,而只作为类型修饰符。
条目2 语言特性export
旧特性:用来定义非内联的模板对象和模板函数。
改动:export特性被移除。export关键字被保留,但是不包含任何语义。
我们可以使用关键字extern来访问其他编译单元中普通类型的变量或对象,而对于模板来说,则需要使用export关键字。export的设计初衷是想要创建一个折中的设计方法,来同时支持模板实例化的包含模型(inclusion model)和独立编译模型(separate compilation model),然而,没有一种增强机制来确保每一种模型的实现都可以很简单。因此,由于实现的难度,很多编译器都没有实现。此外,export关键字在实际编程过程中也很少被用到。
所以,在C++11中,export关键字的语义被移除。但是export仍作为一个无语义的关键字被保留下来。
条目3 register关键字(作为存储类)
旧特性:声明将变量存放在寄存器中。
改动:改变为声明存储类的关键字。
在旧标准中,变量用register来声明时,表示此变量会被大量用到,因此建议将此变量存放在寄存器中,这样可以提高读取的速度。但是,寄存器的数量是有限的,如果寄存器已满,变量依旧会被存放在存储器中。另一方面,它对于编译器来说只是一种建议,而编译器不一定会执行,实际上,大部分编译器都选择忽略它。因此,register关键字其实很少被用到,而且,大多数情况下是没有意义的。
在C++11新标准中,register关键字的作用有所改变。用register关键字仅能用于一个区块内的变量声明或作为函数参数的声明。它仅仅表示变量拥有自动存储的生命期(像C++03中的auto一样)。
条目4 隐式拷贝函数
旧特性:如果类中已经声明了其他拷贝函数或者析构函数,编译器依旧会自动生成一个隐式拷贝函数。
改动:隐式拷贝函数不会自动生成。
在C++11中,如果用户已经声明了一个拷贝复制操作符或者一个析构函数,那么编译器不会隐式声明一个拷贝构造函数。同样,如果用户已经声明了一个拷贝构造函数或者析构函数,编译器则不会隐式地声明一个拷贝复制操作符。
条目5 auto_ptr
旧特性:智能指针,当系统因异常退出时避免资源泄漏。
改动:auto_ptr被unique_ptr所取代。
auto_ptr类模板中存放了一个指针,它指向一个可以通过new得到的对象,并且在此智能指针被析构时向堆归还该对象。这里需要注意的是auto_ptr拥有一个严格的所有权机制。auto_ptr拥有其指针指向的对象的所有权,而复制auto_ptr的操作会复制该指针,并将对象的所有权交给目标类。这是为了避免两个auto_ptr同时拥有同一个对象,否则程序的行为将是不确定的。
在C++11中,unique_ptr提供了一种比auto_ptr更好的解决方案,并取代了auto_ptr。unique_ptr是一个对象,它拥有另一个对象,并且能够通过指针来管理它。更准确地说,unique_ptr对象中有一个指向另一个对象的指针,并且在它自身析构时析构该对象。这些特性都与auto_ptr相同。
此外,unique_ptr也具备了auto_ptr的绝大部分特性,除了auto_ptr的不安全隐性的左值转移(move)。对于auto_ptr来说,拷贝auto_ptr,会导致所有权转移,如以下语句:
std::auto_ptr<int> a(new int);
std::auto_ptr<int> b=a;
同样,拷贝构造函数也会进行所有权的转移,如以下语句:
std::auto_ptr<int> c(a);
由此可见,auto_ptr的转移是隐性的,因此程序员可能会在不经意的情况下就把对象转移了,因此是不安全性。
在unique_ptr中,要进行对象的转移,需要使用std::move函数将对象转换为右值,例如以下语句:
std::unique_ptr<int> a(new int);
std::unique_ptr<int> b=std::move(a);
这是因为unique_ptr对于拷贝行为作了限制。而对于拷贝构造函数来说,unique_ptr并没有类似以下的构造函数:
std::unique_ptr<T>::unique_ptr(std::unique_ptr<T>const&)//默认deleted
如果在构造时想要复制整数的值,可以用以下语句:
std::unique_ptr<int> c(new int(*a));
而如果确实想要将a中的指针进行转移,则需要调用std::move:
std::unique_ptr<int> d(std::move(a));
另外,值得一提的是,unique_ptr可以存放在标准容器之中。
vector<unique_ptr<int> >v;
v.push_back(unique_ptr<int>(new int(0)));
unique_ptr<int> a(new int(0));
v.push_back(move(a));
但是,由于unique_ptr本身不支持拷贝构造,因此元素类型为unique_ptr的容器同样也不支持拷贝构造,这时也需要用到转移构造。
条目6 bind1st/bind2nd
旧特性:将二元函数对象绑定成一元仿函数(函数对象)。
改动:被bind模板所取代。
bind1st和bind2nd函数可以将一个二元函数绑定成一元函数,也就是将二元函数所接受的两个参数之一绑定下来,以此来使函数变成一元的。bind1st绑定第一个参数,bind2nd绑定第二个参数。例如:
find_if(v.begin(),v.end(),bind2nd(greater<int>(),5));
绑定greater<int>的第二个参数为5,亦即找到向量中第一个大于5的整数。
find_if(v.begin(),v.end(),bind1st(greater<int>(),5));
绑定greater<int>的第一个参数为5,亦即找到向量中第一个小于5的整数。
在C++11中,新的bind函数模板提供了一种更好的可调用类的参数绑定机制。
namespace std{
template<class T>struct is_bind_expression
:integral_constant<bool,see below>{};
}
接下来我们来看bind模板函数,它有如下形式:
template<class F,class…BoundArgs>
unspecified bind(F&&f,BoundArgs&&…bound_args);
其中f是函数的右值引用,表示要进行绑定的函数对象,BoundArgs是函数对象的参数类型列表,而bound_args是需要绑定的值。如果一个参数需要绑定,那么在调用bind函数时传具体参数进去即可,而如果不需要绑定,那么就需要使用占位符,std::placeholders::_J,J为从1开始的正整数。bind的返回类型为可调用实体,可以直接赋值给std::function。
如下这个例子:
int Func(int x,int y);
function<int(int)>f=bind(Func,1,placeholders::_1);
f(2);//the same as Func(1,2);
我们可以用is_bind_expression来检查由bind生成的函数对象,而bind也凭借is_bind_expression来检查子表达式。对于用户来说,可以借由它来表示在bind调用中某个类型应该被当做子表达式来对待。如果T是bind的返回类型,那么is_bind_expression由integral_constant<bool,true>得到,否则由integral_constant<bool,false>得到。
另一个值得一提的函数是is_placeholder,它可以检查标准占位符_1、_2等。bind凭借is_placeholder来检查占位符,用户也可以借由此模板来表示占位符类型。如果T的类型是std::placeholders::_J,则is_placeholder<T>由integral_constant<int,J>得到,否则就由integral_constant<int,0>得到。
由上可以看出bind相对于bind1st和bind2nd来说要灵活得多,它不像bind1st和bind2nd那样限制原函数对象的参数个数为两个,bind所接受的函数对象的参数数量没有限制,而且用户可以随意绑定任意个数的参数而不受限制,因此,有了bind,bind1st和bind2nd明显没有了用武之地而被弃用(deprecated)。
条目7 函数适配器(adaptor)
旧特性:ptr_fun,mem_fun,mem_fun_ref,unary_function,binary_function
新特性:弃用。
在旧特性中,提供了多个函数适配器。
template<class Arg1,class Arg2,class Result>
pointer_to_binary_function<Arg1,Arg2,Result>
ptr_fun(Result(*f)(Arg1,Arg2));
ptr_fun的返回值是pointer_to_binary_function<Arg1,Arg2,Result>(f)。简单来说,它可以将一个函数转化为一个函数对象。以下是一个例子:
int compare(const char,const char);
replace_if(v.begin(),v.end(),
not1(bind2nd(ptr_fun(compare),"abc")),"def");
上例将所有v序列中的abc替换为def。
另外,ptr_fun除了可以转化二元函数以外,也可以转化一元函数,此时返回值是pointer_to_binary_function<Arg,Result>(f)。
template<class Arg,class Result>
pointer_to_unary_function<Arg,Result>
ptr_fun(Result(*f)(Arg));
除了常规函数适配器ptr_fun外,还有成员函数适配器mem_fun和mem_fun_ref。
template<class S,class T>class mem_fun_t
:public unary_function<T*,S>
template<class S,class T>mem_fun_t<S,T>mem_fun(S(T::*f)());
template<class S,class T>class mem_fun_ref_t
:public unary_function<T,S>
template<class S,class T>mem_fun_ref_t<S,T>mem_fun_ref(S(T::*f)());
为了说明mem_fun和mem_fun_ref,看一下以下的例子:
void f(C&c);
vector<C>vc;
for_each(vc.begin(),vc.end(),f);
就上例来说代码是可以编译通过的,但是,当f是类C的成员函数时呢?
class C{
public:
void f();
};
此时我们就需要使用到类成员函数适配器了,在这时,mem_fun、mem_fun_ref的区别在于mem_fun需要指针,而mem_fun_ref需要对象的引用。如上述例子中,应该使用mem_fun_ref。
for_each(vc.begin(),vc.end(),mem_fun_ref(&C::f));
mem_fun的用法类似,在此不赘述。由此可见,类成员函数适配器可以将一个不含参数的成员函数转换为一个一元函数,其中参数类型为类本身。此外,它也可以将一个一元成员函数转换为一个二元函数。
mem_fun和mem_fun_ref的返回类型不仅仅只有mem_fun_t和mem_fun_ref_t,而是根据转换后的函数类型不同而有所不同,所有的返回类型如表B-1所示。
现在我们来看C++11的新特性。我们前面已经介绍过了新的bind函数模板,它取代了原有的bind1st和bind2nd。bind不再需要ptr_fun,因此ptr_fun被弃用。而对于mem_fun和mem_fun_ref,C++11提出了一个新的函数模板mem_fn,它实现了mem_fun和mem_fun_ref的所有功能,而且更为强大。它不像mem_fun和mem_fun_ref那样只能处理一元函数或二元函数,它能够针对任意多个参数的函数进行转换;使用时,也不用再区分是指针还是一般对象。因此mem_fun和mem_fun_ref也被弃用,不仅如此,它们所有的返回类型也被弃用。另外,被弃用的还包括unary_function和binary_function。
条目8 动态异常声明(exception specification)
旧特性:异常声明throw()
改动:有参数的异常声明被弃用,空异常声明throw()被noexcept取代。
函数可以通过异常声明来列出它直接或者间接可能抛出的异常。
形如throw(T)的异常声明成为动态异常声明。当一个函数抛出E类型的异常时,如果它的动态异常声明包含一个类型T,且它的处理函数(handler)和类型E是匹配的,那么这个函数就允许E类型的异常。当抛出一个异常时,编译器会搜索处理函数,如果直到最外层的带有异常声明的代码模块都不允许此异常的话,那么如果是动态异常声明,就会调用std::unexpected()。
实践证明,动态异常声明是没有价值的,只能给程序带来更多的开销。它主要的问题如下:
❑C++的异常声明是一种运行时检查,而不是编译时,也就是说,在编译时不能确保所有的异常都能被处理,而运行时的失败模式(failure mode),也就是调用std::unexpected()并不能自身恢复。
❑运行时的检查会要求编译器生成更多代码,而这些代码会阻碍优化,增大运行时开销。
❑在泛型的代码中,很难预知在模板参数的操作过程中会抛出什么类型的异常,所以不太可能写出准确的异常声明。
因此,在C++11中,动态异常声明被弃用。
在动态异常声明中,作为唯一的例外而被认为有价值的是空异常声明,也就是throw()。在实践中,只有两种异常的抛出确实是有用的:程序会抛出异常或者程序不会抛出异常。前者可以由完全省略异常声明来表示;后者则可以由throw()来表示。但是由于性能方面的考虑,还是很少被用到。
在C++11中,提出了一种新的异常声明noexcept,关键字noexcept表示函数不会抛出异常,或者说异常不会被接获并处理。noexcept异常声明除了有noexcept关键字的形式,还可以是noexcept(constant-expression)的形式,这里constant-expression要求可以被转换为bool类型。这样,noexcept可以通过条件判断来决定函数是不是能够抛出异常。另外,noexcept关键字的意义其实就等于noexcept(true)。当用noexcept修饰的函数,也就是不允许抛出异常的函数中抛出异常时,编译器会调用std::terminate()。
与throw()不同,noexcept不需要编译器生成额外的代码来进行运行时检查,而且使用上更为灵活,因此完全可以取代throw()。
综上所述,由于含有参数的动态异常声明在实际使用中没有价值,而空动态异常声明throw()已被noexcept取代,所以动态异常声明,也就是throw(type-id-listopt)被弃用。