C++11相比于C++98增加以许多新特性,让C++语言更加灵活好用,但是貌似也增加了许多学习的难度,现在先看第一部分。
一、右值引用和移动语义
1.右值引用和左值引用
在C++中,值可以大致分为右值和左值,左值大概是哪些已经被定义的变量或者对象,它一般具有持久性,它可以出现在赋值符号的左边,也可以出现在赋值符号的右边;右值一般指的是临时变量、字面常量等等。一般来说,区分左右值的方法可以用是否可以取地址来判断:左值可以取地址,右值不可以被取地址。
下面让我们简单看一段代码吧:
void Func(int& a)
{
cout << "左值引用" << endl;
}
void Func(int&& a)
{
cout << "右值引用" << endl;
}
int main()
{
int a = 1;
Func(1);
Func(a);
return 0;
}
其中,a是一个普通变量,为左值,而1是一个字面常量,为右值,那么最后的输出结果因该是“右值引用”、“左值引用”。让我们看一看结果:
2.右值引用引用左值和左值引用引用右值
右值可以被左值引用引用,因为右值一般具有常性,所以可以通过在左值引用前加上“const”来使得左值引用可以引用右值;而右值引用引用左值则需要使用“move”函数来改变左值的属性。
int main()
{
int a = 2;
const int& L = 1;
int&& R = move(a);
cout << L << endl << R << endl;
return 0;
}
3.右值引用存在的意义
a.延长临时对象的生命周期
右值引用可以延长临时对象的生命周期比如:
int main()
{
string("123456789");
return 0;
}
此代码中,string的生命周期仅限于这一行,倘若使用右值引用来引用,那么就可以延长它的生命周期:
int main()
{
string&& S = string("123456789");
cout << S << endl;
return 0;
}
同样的,表达式相加的临时对象,函数返回时的临时对象,都可以使用右值引用来延长它的生命周期。
b.移动语义
当然了,右值引用存在的意义可不是为了简单的延长生命周期,而是为了转移临时对象的数据,这使得数据的转移更加高效和安全,这也正是移动语义的的机制。在此之前还需要了解的是虽然右值是不可以被改变,但是右值引用是具有左值属性的,也就是说,被右值引用引用的右值是可以被修改的。
拿拷贝构造来说,我们会将它的它的参数写为const类型的,在保证不修改实参的情况下,还能够接收右值。但是无论是左值还是右值,在有资源的情况下需要进行大量的复制行为,特别是在右值的情况下(因为右值的生命周期即将结束,还得进行一次复制,这样会造成效率的低下)。为了解决这个问题,这个时候就需要介绍一下移动构造和移动赋值了。
移动构造和移动赋值旨在将临时对象(右值)的资源转移到我们的类中,由于右值的生命周期即将结束,秉承着趁你病要你命的原则,我是可以在虚弱的时候掠夺你的资源。这个时候我就可以直接把我的没用的东西和你的资源进行交换,这样做的效率可不是一般的高,因为避免了大量的数据拷贝。
移动构造和移动赋值同普通的构造和赋值函数一样,只是参数变为了右值,并且函数内部进行资源的互换。
就用我们的自己写的string类为例。(此处在linux环境下测试,因为VS的编译器会进行优化)
#pragma once
#include<iostream>
using namespace std;
namespace Mynamespace
{
class string
{
friend ostream& operator<<(ostream& _cout, const Mynamespace::string& s);
friend istream& operator>>(istream& _cin, Mynamespace::string& s);
public:
typedef char* iterator;
public:
string(const char* str = "")
{
_str = new char[strlen(str)+1];
char* der = _str;
const char* sour = str;
while (*sour != '\0')
{
*(der++) = *(sour++);
}
*der = '\0';
_size = strlen(str);
_capacity = _size;
}
string(const string& s)
{
_str = new char[s._capacity];
char* der = _str;
const char* sour = s._str;
while (*sour != '\0')
{
*(der++) = *(sour++);
}
*der = '\0';
_size = s._size;
_capacity = s._capacity;
}
string(string&& s)
{
cout << "string(string&& s) 移动构造" << endl;
swap(s);
}
~string()
{
delete[] _str;
_capacity = 0;
_size = 0;
}
string& operator=(const string& s)
{
delete[] _str;
_str = new char[strlen(s._str) + 1];
char* der = _str;
const char* sour = s._str;
while (*sour != '\0')
{
*(der++) = *(sour++);
}
*der = '\0';
_size = s._size;
_capacity = s._capacity;
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
//
// iterator
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
/
// modify
void push_back(char c);
string& operator+=(char c);
void append(const char* str);
string& operator+=(const char* str);
void clear();
void swap(string& s);
const char* c_str()const;
/
// capacity
size_t size()const;
size_t capacity()const;
bool empty()const;
void resize(size_t n, char c = '\0');
void reserve(size_t n);
/
// access
char& operator[](size_t index);
const char& operator[](size_t index)const;
/
//relational operators
bool operator<(const string& s)
{
return strcmp(_str, s._str) < 0;
}
bool operator<=(const string& s)
{
return *this < s || *this == s;
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return !(*this < s);
}
bool operator==(const string& s)
{
return strcmp(_str, s._str) == 0;
}
bool operator!=(const string& s)
{
return !(*this == s);
}
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const;
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const;
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c);
string& insert(size_t pos, const char* str);
// 删除pos位置上的元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len);
private:
char* _str;
size_t _capacity;
size_t _size;
static const size_t npos = -1;
};
}
int main()
{
Mynamespace::string mystring1 = Mynamespace::string("123456789");
return 0;
}
可能是编译器的版本的问题,在我的linux环境中,以上代码即使关闭了优化,也仍然进行直接构造而不是构造+移动构造:
cx@Ubuntu-Linux:~/study$ g++ test.cpp -fno-elide-constructors
cx@Ubuntu-Linux:~/study$ ./a.out
string(const char* str )
有条件的小伙伴可以自己试一试,其中“-fno-elide-constructors”或者“-O0”可以关闭优化。
所以这里先测试一下移动赋值:
int main()
{
Mynamespace::string mystring1;
mystring1 = Mynamespace::string("123456789");
return 0;
}
cx@Ubuntu-Linux:~/study$ g++ test.cpp -fno-elide-constructors
cx@Ubuntu-Linux:~/study$ ./a.out
string(const char* str )
string(const char* str )
string& operator=(string&& s)
可以看到最后调用了一次移动赋值。
二、引用折叠
在C++11中,新增加了引用折叠的特性,引用折叠针对的是引用的引用,但是不可以显式的写出来引用的引用,比如:
int main()
{
int a;
int&& & b = a;
return 0;
}
但是可以隐式的引用(typedef后的引用):
int main()
{
typedef int& L;
typedef int&& R;
int a;
L& b = a;
R& c = a;
R&& d = 1;
L&& e = a;
return 0;
}
引用折叠理解起来有点像与门,在这里,左值引用代表着0,右值引用代表着1,当左值引用右值引用同时存在的时候就是左值引用,只有当两个引用都为右值的时候才为右值引用。
就拿以上代码为例 L& b = a; 里,b的类型为int&, R& c = a; 里,c的类型为int&,R&& d = 1; 里,d 的类型为int&&, L&& e = a; 里 e为左值。
1.万能引用
引用折叠的用途在哪里呢?实际上,它可以用来在函数模板中实现万能引用。请看以下函数模板:
template <class T>
void func(T&& n)
{
cout << "void func(int&& n)" << endl;
}
int main()
{
int a = 1;
func(1);
func(a);
return 0;
}
已知字面常量1是一个右值,变量a是一个左值,由于引用折叠,第一次调用的是右值引用版本的func,第二次是左值引用的func。最后的运行结果如下:
这样可以用一个模板实现左值引用和右值引用的两个版本。
2.完美转发
先看以下代码:
void F(int& a)
{
cout << "左值引用" << endl;
}
void F(int&& a)
{
cout << "右值引用" << endl;
}
void F(const int& a)
{
cout << "左值引用" << endl;
}
template <class T>
void func(T&& n)
{
F(n);
}
int main()
{
int a = 1;
func(a);
func(2);
return 0;
}
我们想要的输出结果是“左值引用”、“右值引用”,那么先看一下运行结果:
哎。为什么是两个左值引用呢?
由于右值引用的属性为左值,当我们想连续传递一个右值的时候,在第一次传递后,这个值就已经变为左值属性了,为了解决这个问题,C++11引入了完美转发,它的目得是为了在右值连续传递的过程中不改变右值的属性。
完美转发的本质是一个函数模板forward,它的底层是一个强转(左值引用和右值引用的本质还是指针,只是在语义上不同)。它的用法是在传递参数的地方加上模板的类型,后边紧接着参数是传递的值:
void F(int& a)
{
cout << "左值引用" << endl;
}
void F(int&& a)
{
cout << "右值引用" << endl;
}
void F(const int& a)
{
cout << "左值引用" << endl;
}
template <class T>
void func(T&& n)
{
F(forward<T>(n));
}
int main()
{
int a = 1;
func(a);
func(2);
return 0;
}
运行结果:
我们可以分析一下:
a.传递的参数为a的时候,a为左值,func发生了引用折叠,编译器推导出T为int&,那么forward强转n为左值并返回。
b.传递的参数为字面常量1的时候,1为右值,func没有发生引用折叠,编译器推导出T为int&&,那么forward强转n为右值并返回。
总的来说,完美转发就是根据T的类型来推导最后返回的类型,如果T是右值,那么最后返回的就是右值属性的对象,如果T为左值,那么最后返回的就是左值属性的对象。