右值引用
1. 移动语义
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造,赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:
class String
{
public:
String(char* str = "")
{
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
String& operator=(const String& s)
{
if (this != &s)
{
char* pTemp = new char[strlen(s._str) + 1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
return *this;
}
~String()
{
if (_str)
delete[] _str;
}
private:
char* _str;
};
假设现在有一个函数,返回值为一个String类型的对象:
String GetString(char* pStr)
{
String strTemp(pStr);
return strTemp;
}
int main()
{
String s1("hello");
String s2(GetString("world"));
return 0;
}
上述代码看起来没有什么问题,但是有一个不太尽人意的地方:GetString函数返回的临时对象,将s2拷贝构造成功之后,立马被销毁了(临时空间的对象被释放),再也没有其他作用;而s2在拷贝构造时,又需要分配空间,一个刚释放一个又申请,有点多此一举。那能否将GetString返回的临时对象的空间直接交给s2呢?这样s2也不需要重新开辟空间了,代码的效率会明显提高
将一个对象中资源移动到另一个对象中的方式,称之为移动语义。在C++11中如果需要实现移动语义,必须使用右值引用。
String(String&& s)
:_srt(s._str)
{
s._str = nullptr;
}
2. C++11中的右值
右值引用,顾名思义就是对右值的引用。C++11中,右值由两个概念组成:纯右值和将亡值。
- 纯右值
- 纯右值是C++98中的概念,用于识别临时变量和一些不跟对象关联的值。比如:常量,一些运算表达式(1+3)等
- 将亡值
- 声明周期将要结束的对象。比如:在值返回时的临时对象
3. 右值引用
右值引用书写格式:
类型&& 引用变量名字=实体;
右值引用最常见的一个使用地方就是:与移动语义结合,减少无必要资源的开辟来提高代码的运行效率。
String&& GetString(char* pStr)
{
String strTemp(pStr);
return strTemp;
}
int main()
{
String s1("hello");
String s2(GetString("world"));
return 0;
}
右值引用另一个比较常见的地方是:给一个匿名对象取别名,延长匿名对象的声明周期。
String GetString(char* pStr)
{
return String(pStr);
}
int main()
{
String&& s = GetString("hello");
return 0;
}
注意:
1.与引用一样,右值引用在定义时必须初始化。
2.通常情况下,右值引用不能引用左值。
int main()
{
int a = 10;
//int&& ra; // 编译失败,没有进行初始化
//int&& ra = a; // 编译失败,a是一个左值
// ra是匿名常量10的别名
const int&& ra = 10;
return 0;
}
4. std::move()
C++11中,std::move()函数位于头文件中,这个函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。注意:被转化的左值,其声明周期并没有随着右值的转化而改变,即std::move转化的左值变量value不会被销毁。
// 移动构造函数
class String
{
//....
String(String&& s)
: _str(s._str)
{
s._str = nullptr;
}
// ....
};
int main()
{
String s1("hello world");
String s2(move(s1));
String s3(s2);
return 0;
}
注意:上述代码是move()误用的一个非常典型的例子,move更多的是用在声明周期即将结束的对象上。
class
{
public:
Person(char* name, char* sex, int age)
: _name(name)
, _sex(sex)
, _age(age)
{}
Person(const Person& p)
: _name(p._name)
, _sex(p._sex)
, _age(p._age)
{}
#if 0
Person(Person&& p)
: _name(p._name)
, _sex(p._sex)
, _age(p._age)
{}
#else
Person(Person&& p)
: _name(move(p._name))
, _sex(move(p._sex))
, _age(p._age)
{}
#endif
private:
String _name;
String _sex;
int _age;
};
Person GetTempPerson()
{
Person p("prety", "male", 18);
return p;
}
int main()
{
Person p(GetTempPerson());
return 0;
}
注意:为了保证移动语义的传递,程序员在编写移动构造函数时,最好使用std::move转移拥有资源的 成员为右值
5 移动语义中的一些问题
1.如果将移动构造函数声明为常右值引用或者返回右值的函数声明为常量,都会导致移动语义无法实现。
String(const String&&); const Person GetTempPerson();
2.在C++11中,无参构造函数/拷贝构造函数/移动贡藕早函数实际上有3个版本
Object() Object(const T&) Object(T &&)
3.C++11中默认成员函数
默认情况下,编译器会为程序员隐式生成一个(如果没有用到则不会生成)移动构造函数。如果程序员声明了自定义的构造函数,移动构造,拷贝构造函数,赋值运算符重载,移动赋值,析构函数,编译器都不会再为程序员生成默认版本。编译器生成的默认移动构造函数实际和默认的拷贝构造函数类似,都是按照位拷贝(即浅拷贝)来进行的。因此,在类中涉及到资源管理时,程序员最好自己定义移动构造函数。其他类有无移动构造都无关紧要。注意:在C++11中,拷贝构造/移动构造/赋值/移动赋值函数必须同时提供,或者同时不提供,程序才能保证类同时具有拷贝和移动语义。
6. 完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
void Func(int x)
{
// ......
}
template<typename T>
void PerfectForward(T t)
{
Fun(t);
}
PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,他就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
C++11通过forword函数来实现完美转发,比如:
void Fun(int &x){ cout << "lvalue ref" << endl; }
void Fun(int &&x){ cout << "rvalue ref" << endl; }
void Fun(const int &x){ cout << "const lvalue ref" << endl; }
void Fun(const int &&x){ cout << "const rvalue ref" << endl; }
template<typename T>
void PerfectForward(T &&t){ Fun(std::forward<T>(t)); }
int main()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}