自学内容网 自学内容网

C++11 右值引用和移动语义

目录

1.左值引用和右值引用

2.右值引用使用场景(移动语义)和意义

3.右值引用引用左值及其一些更深入的使用场景分析

4.完美转发 


1.左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
 

int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}

什么是右值?什么是右值引用?

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回),匿名对象,临时对象等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
 

int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}

左值能否给右值取别名?---不可以,但是const左值引用可以。

​
const int& rr1l = 10;
const double& rr2l= x + y;
const double& rr3l = fmin(x, y); 

​

右值引用能否给左值取别名---不可以,但是可以给move以后的左值取别名。

int* p = new int(0);
int*&& rp = move(p);


2.右值引用使用场景(移动语义)和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

左值引用的使用场景:做参数和做返回值都可以提高效率。
 

左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:mystring::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

namespace mystring 
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}

iterator end()
{
return _str + _size;
}

string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str) -- 构造" << endl;

_str = new char[_capacity + 1];
strcpy(_str, str);
}

// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}

// 拷贝构造
// 左值
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;

_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}


// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);

delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;

return *this;
}

~string()
{
delete[] _str;
_str = nullptr;
}

char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}

void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;

_capacity = n;
}
}

void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}

_str[_size] = ch;
++_size;
_str[_size] = '\0';
}

//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}

const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};

mystring::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}

mystring::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}

std::reverse(str.begin(), str.end());

return str;
}
}

        只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

int main()
{
mystring::string ret1 = mystring::to_string(1234);
cout << ret1.c_str() << endl;

return 0;
}

两次拷贝构造,效率低。

右值引用和移动语义解决上述问题:
在mystring::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

下面的就是移动构造,与拷贝构造构成函数重载。

// 移动构造
// 右值(将亡值)
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}

        to_string 的返回值是一个临时对象(将亡值)也就是右值,用这个右值构造ret2,如果既有拷贝构造又有移动构造,调用就会匹配调用移动构造,因为编译器会选择最匹配的参数调用。那么这里就是一个移动语义。

       在编译器优化的情况下,to_string函数的返回值,会被编译器强制move变成右值,同样的调用移动构造,两次移动构造,编译器会再次优化,合成一次移动构造,直接使用str构造ret1,这里就不存在拷贝了,直接将右值的资源转移过来,提高了效率。

不仅仅有移动构造,还有移动赋值:
在mystring::string类中增加移动赋值函数,再去调用mystring::to_string(1234),不过这次是将mystring::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

string& operator=(string&& s)
{
cout << "string(string&& s) -- 移动赋值" << endl;
swap(s);

return *this;
}
        int main()
        {
            mystring::string ret1;
            ret1 = bit::to_string(1234);
            return 0;
        }
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义

这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。mystring::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为mystring::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。


        我们在这里思考一个问题,在移动构造中,s是右值,我们在调用swap的时候,还是可以获取s的资源,但右值是不用可以被修改的?

        实际上,右值引用的属性本身是左值:一旦一个右值被绑定到一个右值引用上,这个右值引用本身就是一个左值,因为它有一个明确的存储位置。这样的意义,是为了移动构造移动赋值,转移资源的语法逻辑自洽。只有右值引用本身处理成左值,才能实现移动构造和移动赋值,转移资源。

3.右值引用引用左值及其一些更深入的使用场景分析

C++11引入了右值引用和移动语义,以支持更加高效的资源管理和对象操作。

右值引用

右值引用是C++11引入的一种新引用类型,其语法形式为T&&,其中T是某个类型。右值引用主要用于绑定到右值(如临时对象、字面量等),从而允许程序员对这些右值进行直接操作,避免不必要的拷贝。

移动语义

移动语义允许一个对象将其资源“移动”到另一个对象,而不是进行传统的拷贝。这样做的好处是避免了不必要的资源分配和析构,从而提高了程序的性能。为了实现移动语义,C++11中引入了std::move函数模板和移动构造函数以及移动赋值运算符。

int main()
{
    string s1("111111111111");
    string s3 = move(s1);
}

资源转移,而非简单的拷贝。

std::move

std::move是一个函数模板,它将其参数转换为右值引用。但是,std::move并不真正移动任何数据,它只是简单地执行一个类型转换。实际上,数据的移动(如果发生)是由移动构造函数或移动赋值运算符来完成的。

        按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// forward _Arg as movable
return ((typename remove_reference<_Ty>::type&&)_Arg);
}
int main()
{
mystring::string s1("hello world");
// 这里s1是左值,调用的是拷贝构造
mystring::string s2(s1);
// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
// 资源被转移给了s3,s1被置空了。
mystring::string s3(std::move(s1));
return 0;
}

STL容器插入接口函数也增加了右值引用版本:

void push_back(value_type&& val);
int main()
{
list<mystring::string> lt;
mystring::string s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("2222");
lt.push_back(std::move(s1));
return 0;
}
运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义

        上面所作的效率提升,针对的是自定义类型的深拷贝的类,深拷贝的类才有转移资源的移动系列的函数。像日期类(浅拷贝自定义类型),内置类型不存在资源转移一说


4.完美转发 

        模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值.模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发

eg:接收的类型都退化成了左值

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }

void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}

eg:使用std::forward 完美转发在传参的过程中保留对象原生类型属性

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}


原文地址:https://blog.csdn.net/2301_76618602/article/details/139770456

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!