C++11中的移动语义
1.移动构造函数
移动语义就是使用移动构造函数来构造对象。
我们知道在类中如果存在指针数据成员,那么我们就一定要写拷贝构造函数,进行深拷贝
如下所示,就是拷贝构造函数的用法:
#include<iostream>
using namespace std;
class A
{
public:
int * ptr;
A():ptr(new int(0))
{
cout<<"constructor\n";
};
A(const A & h):ptr(new int(*h.ptr))//拷贝构造函数
{
cout<<"copy constructor\n";
//deep copy
};
~A()
{
cout<<"destructor\n";
delete ptr;
};
};
A getA()
{
return A();
}
int main()
{
A a=getA();
}
//g++ .\test.cpp -std=c++11 -fno-elide-constructors
constructor
copy constructor
destructor
copy constructor
destructor
destructor
可以知道上面代码中,实际上产生了3个对象,在
getA()
函数中,使用默认构造函数产生一个对象,然后将其作为返回值时,又会通过拷贝构造函数产生一个对象,然后在main()
函数中,又会通过拷贝构造函数构造出对象a
,所以总共有3个对象产生,我们这里的拷贝构造函数是进行的深拷贝,所以就会开辟3块内存.
在C++11中,我们可以使用移动构造函数,对上述代码进行优化
#include<iostream>
using namespace std;
class A
{
public:
int * ptr;
A():ptr(new int(0))
{
cout<<"constructor\n";
};
A(const A & h):ptr(new int(*h.ptr))//拷贝构造函数
{
cout<<"copy constructor\n";
//deep copy
};
A(A && h):ptr(h.ptr)//移动构造函数
{
h.ptr=nullptr;
cout<<"move constructor\n";
}
~A()
{
cout<<"destructor\n";
delete ptr;
};
};
A getA()
{
return A();
}
int main()
{
A a=getA();
}
//g++ .\test.cpp -std=c++11 -fno-elide-constructors
constructor
move constructor
destructor
move constructor
destructor
destructor
移动构造函数,它是进行的浅拷贝,由于被移动的值会立即进行析构,所以我们不关心它,只需进行浅拷贝,将其开辟的内存空间转让给别人。上述代码中,也会构造出3个对象,但是它们只开辟一块内存空间,这就是移动构造函数的优势。
总之,我们发现移动构造函数和拷贝构造函数的区别,其实就是深拷贝和浅拷贝的区别,移动构造函数的开销更小,当然我们关心的是,移动构造函数何时会被触发?在上面代码中就是一个例子,将getA()
中的局部匿名对象移动给返回值,然后将返回值移动给 main()
中的a
。
这里我们给出结论:移动构造函数只有在使用右值来构造对象时才会调用
那么什么是右值?
在
getA()
中的A()
就是右值,getA()
的返回值也是右值,所以用它们构造对象时,会调用移动构造函数
2.右值引用
在C++11中,我们将值划分为:左值、右值(分为将亡值和纯右值)
左值:可以取地址,有名字的值
右值:不能取地址,没有名字的值
纯右值:运算表达式,如1+2
,或者和对象无关的字面值,如true
,或者非引用的函数返回值,或者lambda
表达式
将亡值:仅和右值引用相关的值,它包括:右值引用的函数返回值T&&
,或者std::move
的返回值,或者被转换为T&&
类型的函数返回值
注意:不管是纯右值还是将亡值,它们的存活时间都很短。不要被将亡值的名称所迷惑了,其实所有右值的都会即将消亡。
实际上,对于纯右值和将亡值的定义很难给出,而且我们也不需要区分它们两,但是,我们至少可以确定一个值是左值还是右值。
C++98中所提及的引用,在C++11中我们称之为左值引用,即这个引用只能绑定左值,在C++11中我们提供了一种新的能够绑定右值的引用,即右值引用。
我们知道左值引用实际是一个变量的别名,右值引用它实际是一个匿名变量的别名
#include<iostream>
using namespace std;
class A
{
public:
int * ptr;
A():ptr(new int(0))
{
cout<<"constructor\n";
};
A(const A & h):ptr(new int(*h.ptr))//拷贝构造函数
{
cout<<"copy constructor\n";
//deep copy
};
A(A && h):ptr(h.ptr)//移动构造函数
{
h.ptr=nullptr;
cout<<"move constructor\n";
}
~A()
{
cout<<"destructor\n";
delete ptr;
};
};
A getA()
{
return A();
}
int main()
{
A&& a=getA();//右值引用
}
constructor
move constructor
destructor
destructor
在上述代码中
getA()
的返回值是一个右值,它是一个临时值,如果我们写成A a=getA();
,那么这个临时值给a
进行移动构造后就会立即被析构,而如果我们使用A&& a=getA();
,那就意味著我们给这个临时值进行续命,a
就是这个临时值的别名,所以上述代码就会少一个对象的构造。
总之,右值引用就是一种绑定右值的引用,实际上在C++98中,我们所知的const T &
,这样的引用,也可以绑定右值,他也叫做万能引用,当他绑定右值的时候它的作用和右值引用是一样的,只不过这里的const
是底层的,所以我们不能用其修改右值,所以右值引用绑定右值时,可以修改该右值,而当万能引用绑定右值时,我们不可以修改该右值
T& a;//左值引用,只能绑定非常量左值
T&& a;//右值引用,只能绑定非常量右值
const T& a;//万能引用,它可以绑定一切值,但是它不能修改该值
const T&& a;//和万能引用功能一样(一般不使用)
我们仔细来思索一下右值引用的用处,从本质上讲,它是给右值进行续命,而从实践上讲,它就是用来移动语义的,但是移动语义的时候,我们希望修改原来的右值(看上面代码中的移动构造函数,它实际上修改了右值),所以我们说const T&&
这种是无用的,
我们在学习了C++11中的移动语义和右值引用知识后,我们要深知一个编程规矩:
只要类中有指针数据成员,就一定要重写拷贝构造函数和移动构造函数
3.右值引用本身是左值
我们知道在C++98中,左值引用本身是个左值,同样的,在C++11中,我们规定右值引用本身是左值
int num=1;
int &a =num;
int &b=a;
int &&c =2;
int && d=c;//报错
上面代码中只有最后一行会报错,因为虽然
c
是右值2
的引用,但是c
本身是一个左值,也可以这样理解,右值引用采用借尸还魂的方式,赐予右值一个名字,从而给右值续命。
void foo(int &&);
int main()
{
int &&a=1;
foo(a);//报错
}
同样的,上面代码也会报错,因为非常量右值引用只能绑定非常量右值,而
a
是一个左值
当右值引用作为函数返回值时,它却是右值
int && foo()
{
return 1;
}
int main()
{
int && b=foo();
}
上面代码是正确的
这实际就是将亡值的定义::右值引用的函数返回值T&&,或者std::move的返回值,或者被转换为T&&类型的函数返回值
4.std::move()
将左值强制转换为将亡值
实际上,std::move()
等价于static_cast<T&&>()
。
看一下下面这段代码
#include<iostream>
using namespace std;
class A
{
public:
int * ptr;
A():ptr(new int(0))
{
cout<<"constructor\n";
};
A(const A & h):ptr(new int(*h.ptr))//拷贝构造函数
{
cout<<"copy constructor\n";
//deep copy
};
A(A && h):ptr(h.ptr)//移动构造函数
{
h.ptr=nullptr;
cout<<"move constructor\n";
}
~A()
{
cout<<"destructor\n";
delete ptr;
};
};
A&& getA()
{
return std::move(A());
}
int main()
{
A&& a=getA();
}
constructor
destructor
在
getA()
中的A()
是右值,为什么还要用std::move
将其转换为右值?因为A()
是一个纯右值,右值引用当然可以绑定纯右值,但是A()
是一个局部对象,在函数中返回引用时,我们禁止返回局部对象的引用,但是当我们使用std::move
后,A()
就会转换为将亡值,这样子就可以将其作为引用返回。这是一种返回局部对象引用的特殊方法。
注意,这是一个涉及原则的问题,匿名对象是纯右值
class A
{};
int main()
{
A& a=A();//报错,左值引用无法绑定纯右值
}
但是,std::move()
有一个bug
,即被转化为右值的左值,不会被立即析构。
#include<iostream>
using namespace std;
class A
{
public:
int* ptr;
A():ptr(new int(999)){}
~A(){delete ptr;}
A(const A& h):ptr(new int(*h.ptr)){}
A(A&& h):ptr(h.ptr)
{
h.ptr=nullptr;
}
};
int main()
{
A a;
A b(std::move(a));
cout<<*a.ptr<<endl;//报错
}
上述代码就会报错,因为
a
被转化为右值引用后,b
会调用移动构造函数来构造它自己,而在移动构造函数中,它将a.ptr
置空
#include<utility>
class A
{
public:
int *ptr;
A():ptr(new int(0)){}
~A(){delete ptr;}
A(const A& h):ptr(new int(*h.ptr)){}
A(A&& h):ptr(h.ptr){h.ptr=nullptr;}
};
class B
{
public:
int *ptr;
A elem;
B():ptr(new int(0)){}
~B(){delete ptr;}
B(const B&h):ptr(new int(*h.ptr)),elem(h.elem){}
B(B&& h):ptr(h.ptr),elem(std::move(h.elem)){h.ptr=nullptr;}
};
注意看,
B(const B&h):ptr(new int(*h.ptr)),elem(h.elem){}
中对elem
的初始化使用的是A
的拷贝构造函数,
而B(B&& h):ptr(h.ptr),elem(std::move(h.elem)){h.ptr=nullptr;}
中对elem
的初始化使用的是是A
的移动构造函数.
注意一点,即使这里我们忘记写std::move()
也并无大碍,它会自行调用拷贝构造函数,当然这也会导致一些开销,所以在做类开发的时候,在写类的移动构造函数的时候,总是要记得将类成员move
成右值。
5.拷贝语义和移动语义
如果一个类支持拷贝构造函数和拷贝赋值函数,那么我们就称该类具有拷贝语义;同样的如果一个类支持移动构造函数和移动赋值函数,那么我们就称该类具有移动语义。
当然有些类是同时支持移动语义和拷贝语义的。
在C++98中的类基本都是只具有拷贝语义的,而在C++11中的基本所有类都支持移动语义,特别的,有些类只支持移动语义,而不支持拷贝语义,这种类,我们称之为资源型类,即资源只能被移动而不能被拷贝,例如智能指针类unique_ptr
,文件流ifstream
等都是资源型类,在C++11中,我们可以通过一些工具来判断一个类是否支持移动语义。
我们看一下下面的代码
template <class T>
void swap(T& a, T& b)
{
T tmp(move(a));
a=move(b);
b=move(tmp);
}
上述代码中,如果
T
支持移动语义,那么它就会调用移动构造函数和移动赋值函数,而如果T
只支持拷贝语义,那么它也可以调用拷贝构造函数和拷贝赋值函数
我们关于移动语义的另一个话题是:异常。因为如果移动语义没有完成,却抛出异常,那么可能会导致产生悬挂指针。所以在C++11中我们同样有std::move_if_noexcept()
函数来检测,移动构造函数是否用noexcept
修饰。
再讨论一个关于编译器优化的问题,如今c++编译器已经非常优化了,RVO机制,即所谓返回值优化机制,他能帮你完成类似移动语义的智能优化,但是要记住,编译器优化不是完全奏效的,最好还是自己提高代码效率。