C++ 11完美转发C++ 11中引入的一个非常重要也是比较难于理解的新特性就是完美转发(Perfect Forwarding)。完美转发中有两个关键词:“转发”和“完美”。
我们先来看第一个关键词“转发”,那么在C++中,“转发”表示什么含义呢?转发通常用在模板编程中,假设有一个函数F(a1, a2, ..., an),如果存在另一个函数G(a1, a2, ..., an),调用G相当于调用了F,则我们说函数G将a1, a2, ..., an等参数正确地转发给了函数F。再来看第二个关键词“完美”,“完美”转发是相对于其他转发方案而言的。在目前已提出的7种转发方案中,只有完美转发能够正确地实现转发的语义,其他方案都存在这样或那样的问题。下面一一进行介绍。
转发方案一:使用非常量左值引用。考虑下面的代码。
1 void F(int a)
2 {
3 cout << a << endl;
4 }
5
6 template<class A>
7 void G(A &a)
8 {
9 F(a);
10 }
使用非常量左值引用时,我们可以调用F(10),但无法调用G(10),即我们无法接收非常量右值的参数。
转发方案二:使用常量左值引用。考虑下面的代码。
1 void F(int &a)
2 {
3 cout << a << endl;
4 }
5
6 template<class A>
7 void G(const A &a)
8 {
9 F(a);
10 }
使用常量左值引用时,函数G可以接收任意类型的值作为参数,包括非常量左值、常量左值、非常量右值和常量右值。但当F的参数类型为非常量左值引用时,我们无法将一个常量左值引用转发给一个非常量左值引用。
转发方案三:使用非常量左值引用 + 常量左值引用。考虑下面的代码。
1 template<class A>
2 void G(A &a)
3 {
4 F(a);
5 }
6
7 template<class A>
8 void G(const A &a)
9 {
10 F(a);
11 }
综合前面两种方案的分析结果,可以得出这种方案相当于对函数G进行了重载,此时可以接收任意类型的值作为参数,也可以顺利地实现转发。但由于使用了常量和非常量两种形式的重载,当参数的个数N较大时,需要重载的函数会呈指数级增长(2的N次方),因此这种方案实际上是不可取的。
转发方案四:使用常量左值引用 + const_cast。
1 template<class A>
2 void G(const A &a)
3 {
4 F(const_cast<A &>(a));
5 }
这种方案克服了方案二的缺点,现在可以将常量左值引用转发给非常量左值引用了。但这又带来了新的问题,假如F的参数是一个非常量左值引用,则调用G后,我们可以通过F来修改传入的常量左值和常量右值了,而这是非常危险的。
转发方案五:非常量左值引用 + 修改的参数推导规则。
这种方案与方案一类似,但需要修改现有的参数推导规则,即传递一个非常量右值给模板类型时,将它推导成常量右值,这样就解决了方案一中无法接收非常量右值的参数的问题。但由于修改了现有的参数推导规则,因此会导致已有代码的语义发生改变。考虑下面的代码。
1 template<class A>
2 void F(A &a)
3 {
4 cout << "void F(A& a)" << endl;
5 }
6
7 void F(const long &a)
8 {
9 cout << "void F(const long &a)" << endl;
10 }
在未修改参数推导规则前,调用F(10)会选择第二个重载函数,但修改后,却会调用第一个重载函数,这就给C++带来了兼容性的问题。
转发方案六:右值引用。考虑下面的代码。
1 template<class A>
2 void G(A &&a)
3 {
4 F(a);
5 }
在这种方案中,G将无法接收左值,因为不能将一个左值传递给一个右值引用。另外,当传递非常量右值时也会存在问题,因为此时a本身是一个左值,这样当F的参数是一个非常量左值引用时,我们就可以来修改传入的非常量右值了。
转发方案七:右值引用 + 修改的参数推导规则。
要理解修改后的参数推导规则,我们先来看一下引用叠加规则:
1、T& + & = T&
2、T& + && = T&
3、T&& + & = T&
4、T或T&& + && = T&&
修改后的针对右值引用的参数推导规则为:若函数模板的模板参数为A,模板函数的形参为A&&,则可分为两种情况讨论:
1、若实参为T&,则模板参数A应被推导为引用类型T&。(由引用叠加规则第2点T& + && = T&和A&&=T&,可得出A=T&)
2、若实参为T&&,则模板参数A应被推导为非引用类型T。(由引用叠加规则第4点T或T&& + && = T&&和A&&=T&&,可得出A=T或T&&,强制规定A=T)
应用了新的参数推导规则后,考虑下面的代码。
1 template<class A>
2 void G(A &&a)
3 {
4 F(static_cast<A &&>(a));
5 }
当传给G一个左值(类型为T)时,由于模板是一个引用类型,因此它被隐式装换为左值引用类型T&,根据推导规则1,模板参数A被推导为T&。这样,在G内部调用F(static_cast<A &&>(a))时,static_cast<A &&>(a)等同于static_cast<T& &&>(a),根据引用叠加规则第2点,即为static_cast<T&>(a),这样转发给F的还是一个左值。
当传给G一个右值(类型为T)时,由于模板是一个引用类型,因此它被隐式装换为右值引用类型T&&,根据推导规则2,模板参数A被推导为T。这样,在G内部调用F(static_cast<A &&>(a))时,static_cast<A &&>(a)等同于static_cast<T&&>(a),这样转发给F的还是一个右值(不具名右值引用是右值)。
可见,使用该方案后,左值和右值都能正确地进行转发,并且不会带来其他问题。另外,C++ 11为了方便转发的实现,提供了一个函数模板forward,用于参数的完美转发。使用forward后的代码可简化为:
1 template<class A>
2 void G(A &&a)
3 {
4 F(forward<A>(a));
5 }
为了便于进行各种转发方案的比较,下面以表格的形式列出了各自的特性。
转发方案非常量左值常量左值非常量右值常量右值修改语言已知问题
一、非常量左值引用非常量左值常量左值无法转发常量左值否无法接收非常量右值的参数
二、常量左值引用常量左值常量左值常量左值常量左值否无法将常量左值引用转发给非常量左值引用
三、非常量左值引用 + 常量左值引用非常量左值常量左值常量左值常量左值否重载函数过多,实际编码不可行
四、常量左值引用 + const_cast非常量左值非常量左值非常量左值非常量左值否可修改常量左值和常量右值,不安全
五、非常量左值引用 + 修改的参数推导规则非常量左值常量左值常量左值常量左值是会导致兼容性问题,且不支持移动语义
六、右值引用无法转发无法转发非常量左值常量左值是可修改非常量右值,不安全
七、右值引用 + 修改的参数推导规则非常量左值常量左值非常量右值常量右值是暂无,故简称为完美转发
注:关于左值引用和右值引用,可以参考我的另一篇文章:
【原】C++ 11右值引用
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请
点击举报。