[C++11] 右值引⽤与移动语义

文章目录

  • 左值和右值
    • 左值(Lvalue)
    • 右值(Rvalue)
    • 区别
  • 左值引⽤和右值引⽤
    • 左值引用(Lvalue Reference)
    • 右值引用(Rvalue Reference)
      • 右值引用的特点
  • 右值引用延长生命周期
  • 右值引⽤和移动语义的使⽤ (重点)
    • 左值引用的主要使用场景回顾
    • 移动构造函数与移动赋值操作符
      • 定义
      • 代码示例
    • 右值引⽤和移动语义解决传值返回问题
      • 右值对象构造,只有拷⻉构造,没有移动构造的场景
      • 右值对象构造,有拷⻉构造,也有移动构造的场景
      • 右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景
      • 右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景
    • 右值引⽤和移动语义在传参中的提效
  • 类型分类
    • 左值(Lvalue)
    • 右值(Rvalue)
    • 纯右值(Prvalue)
    • 将亡值(Xvalue)
    • 泛左值(Glvalue)
  • 引用折叠
    • 什么是引用折叠?
    • 为什么需要引用折叠?
    • 引用折叠的应用示例
      • 函数模板
      • typedef 引用折叠
  • 完美转发完美转发:保持函数参数的值类别
    • 完美转发的背景
    • `std::forward` 的实现
    • 示例代码分析
      • 流程分析

左值和右值

在C++中,左值(lvalue)和右值(rvalue)是两种不同的表达式类型,它们的主要区别在于它们在内存中的状态和使用方式。

左值(Lvalue)

左值是指那些在内存中有持久存储位置的对象。它们通常代表对象的身份,即它们有一个明确的内存地址,并且可以通过这个地址进行读写操作。左值可以出现在赋值操作的左边或右边。

特征:

  • 可以取地址(即可以使用&操作符获取其内存地址)。
  • 可以被赋值。
  • 可以作为非常量引用的绑定对象。

例子:

int a = 10; // 'a' 是一个左值,因为它有一个持久的存储位置。
int* p = &a; // 取'a'的地址,'p'现在指向'a'的存储位置。
a = 20; // 'a' 可以被赋值。

右值(Rvalue)

右值是指那些在内存中没有持久存储位置的对象,通常是临时的,比如字面量、表达式的计算结果等。右值代表的是值本身,而不是值所在的内存位置。右值不能被赋值,也不能取地址。

特征:

  • 不能取地址(尝试获取右值的地址会导致编译错误)。
  • 不能被赋值。
  • 通常用作右值引用的绑定对象,以实现移动语义。

例子:

int b = 30; // 'b' 是一个左值。
int c = b * 2; // 'b * 2' 是一个右值,因为它是一个表达式的计算结果。

区别

  1. 持久性:左值指向内存中的持久对象,而右值通常是临时的,表达式结束后就会被销毁。
  2. 可变性:左值可以被重新赋值,而右值通常不能。
  3. 地址:左值可以取地址,而右值不可以。

左值和右值的核⼼区别就是能否取地址

左值引⽤和右值引⽤

左值引用(Lvalue Reference)

  • 定义:左值引用用于引用可以取地址的变量,即具有持久性存储的对象。例如,变量、数组元素、解引用指针等都是左值。
  • 语法:Type& r1 = x; 这里的 r1 是对 b 的左值引用。
  • 常见的左值引用:
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
double x = 1.1, y = 2.2;

右值引用(Rvalue Reference)

  • 定义:右值引用用于引用那些临时对象或不可取地址的对象。右值通常是字面量、表达式结果等。
  • 语法:Type&& rr1 = 10; 这里的 rr1 是对右值 10 的右值引用。

右值引用的特点

  1. 不能直接引用左值
  2. 右值引用不能绑定到左值,因为左值的生命周期比右值长。
  3. 左值引⽤不能直接引⽤右值,但是<font style="color:rgb(31,35,41);">const</font>左值引⽤可以引⽤右值
  4. 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤<font style="color:rgb(31,35,41);">move(左值)</font>
    • 例:
// int&& rrx1 = b;  // 错误// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
  1. 可以引用通过 std::move 转换的左值
  2. <font style="color:rgb(31,35,41);">move</font>是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换
  3. <font style="color:rgb(31,35,41);">template <class T> typename remove_reference<T>::type&& move (T&& arg); </font>
  4. std::move 将左值强制转换为右值引用,允许右值引用绑定到左值。例如:
int&& rrx1 = move(b); // 通过move将b转换为右值引用
  1. 变量表达式属性
  • 所有变量表达式(包括右值引用变量)都是左值属性,意味着它们可以被取地址。⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");

也就是说以上的rr皆为左值。

右值引用延长生命周期

右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆

法被修改。

std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:不能绑定到左值const std::string& r2 = s1 + s1; // OK:到 const 的左值引用延⻓生存期
// r2 += "Test"; // 错误:不能通过到 const 的引⽤修改std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期
r3 += "Test"; // OK:能通过到非const 的引⽤修改std::cout << r3 << '\n';

  • C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
  • C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。

void f(int& x)
{std::cout << "左值引用重载 f(" << x << ")\n";
}void f(const int& x)
{std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}void f(int&& x)
{std::cout << "右值引用重载 f(" << x << ")\n";
}int main()
{int i = 1;const int ci = 2;f(i); // 调⽤ f(int&)f(ci); // 调⽤ f(const int&)f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)f(std::move(i)); // 调⽤ f(int&&)// 右值引⽤变量在⽤于表达式时是左值int&& x = 1;f(x); // 调⽤ f(int& x)f(std::move(x)); // 调⽤ f(int&& x)return 0;
}

右值引⽤和移动语义的使⽤ (重点)

左值引用的主要使用场景回顾

左值引用主要的使用场景是在函数中通过左值引用传递返回值的时候减少拷贝或者在传参的时候用左值引用接收实参减少拷贝,并且还可以修改接收的实参。

左值引用已经解决了大部分效率问题,但是在有些情况下还是无法完全解决并且可能造成错误。例如在addStringgenerate函数,如果使用左值引用接收返回的对象的话则会得到一个已经析构的对象,因为该对象已经离开了创建时所在的作用域,导致引用的空间也被释放。

string addStrings(string num1, string num2)
{string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;// 进位int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1)str += '1';reverse(str.begin(), str.end());return str;
}

通过C++98的方法可以通过增加参数多传入一个提前创建好的对象的引用,然后在函数中直接对该对象进行构造来避免多次拷贝造成效率上的浪费。

string addStrings(string num1, string num2, string& str)
{......
}string str;
string addStrings(s1, s2, str); // 直接传入str在内部进行构造

那么在这个时候能用右值引用来解决吗?

上文已经提出:右值引用可以延长对象的生命周期,并且恰好可以直接返回右值来避免再次构造对象。

实践证明,使用右值引用来接收返回值则会收到空的内容。但是右值引用不是可以延长右值的生命周期吗,为什么还是内容被销毁。

实际上,右值引用确实可以延长右值的生命周期,但是返回的右值是在构造的函数栈帧中建立的空间,当使用完函数后栈帧会被释放,当然右值的空间也会被释放,所以即使接受了返回值,接收的也是空值。

所以可以引出移动语意

移动构造函数与移动赋值操作符

定义

  1. 移动构造函数
    • 定义:移动构造函数接受一个右值引用作为参数,并通过“窃取”资源来初始化对象。
    • 语法ClassName(ClassName&& other) noexcept
    • 目的:避免不必要的深拷贝,提高性能。
  2. 移动赋值操作符
    • 定义:移动赋值操作符重载,允许将一个右值引用的对象赋值给当前对象。
    • 语法ClassName& operator=(ClassName&& other) noexcept
    • 目的:同样避免不必要的拷贝,提高效率。

代码示例

含有移动构造函数和移动赋值运算符重载的my_string 类模拟实现

class string {
public:// 构造函数string(const char* str = ""): _size(strlen(str)), _capacity(_size) {// 资源分配}// 拷贝构造函数string(const string& s) : _str(nullptr) {// 深拷贝实现}// 移动构造函数string(string&& s) {cout << "string(string&& s) -- 移动构造" << endl;swap(s); // 窃取资源}// 移动赋值操作符string& operator=(string&& s) {cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s); // 窃取资源return *this;}// 交换成员函数void swap(string& other) noexcept {char* tmp = this->_str;this->_str = other._str;other._str = tmp;}// 析构函数~string() {delete[] _str; // 释放资源}
};
  • 构造函数:动态分配内存并初始化 _str
  • 拷贝构造函数:实现深拷贝,通过逐字符复制。
  • 移动构造函数
    • swap(s) 窃取 s 的资源,避免深拷贝。
    • 在完成构造后,s 进入一个有效但未定义的状态。
  • 移动赋值操作符
    • 同样使用 swap(s),在赋值前确保清理当前对象的资源。
    • 通过 noexcept,确保在发生异常时程序的安全性。

测试main 函数:

int main() {my_string::string s1("xxxxx");my_string::string s2 = s1; // 拷贝构造my_string::string s3 = my_string::string("yyyyy"); // 移动构造优化my_string::string s4 = move(s1); // 移动构造return 0;
}
  1. s1 的初始化

my_string::string s1("xxxxx"); 这行代码调用了构造函数,创建了一个新的 my_string 对象 s1。这里使用的是普通构造函数,而不是移动构造。

  1. 拷贝构造

my_string::string s2 = s1; 这行代码使用了拷贝构造函数,因为 s1 是一个左值(它有名字且可以取地址)。因此,拷贝构造函数被调用,复制 s1 的内容到 s2

  1. 移动构造优化

my_string::string s3 = my_string::string("yyyyy"); 这里,my_string::string("yyyyy") 是一个临时对象(右值),因此会触发移动构造函数的调用。编译器会优化这一步骤,直接通过移动构造来初始化 s3

  1. 移动构造

my_string::string s4 = move(s1); 使用了 std::move,这将 s1 转换为右值引用,使得移动构造函数被调用。此时,s1 的资源被“窃取”,而 s1 进入一个有效但未定义的状态。

右值引⽤和移动语义解决传值返回问题

#define _CRT_SECURE_NO_WARNINGS 1
#include<string>
#include<algorithm>
#include<assert.h>
#include <iostream>
using namespace std;string&& addStrings(string num1, string num2)
{string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;// 进位int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1)str += '1';reverse(str.begin(), str.end());return move(str);
}namespace my_string
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{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);}string(const string& s):_str(nullptr){cout << "string(const string& s) -- 拷⻉构造" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}// 移动构造string(string&& s){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}void swap(string& other) noexcept{char* tmp = this->_str;this->_str = other._str;other._str = tmp;}~string(){cout << "~string() -- 析构" << endl;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];if (_str){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){push_back(ch);return *this;}const char* c_str() const{return _str;}size_t size() const{return _size;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;};string addStrings(string num1, string num2){string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1)str += '1';reverse(str.begin(), str.end());cout << "******************************" << endl;return str;}
}// 场景1
int main()
{my_string::string ret = my_string::addStrings("11111", "2222");cout << ret.c_str() << endl;return 0;
}// 场景2
int main()
{my_string::string ret;ret = my_string::addStrings("11111", "2222");cout << ret.c_str() << endl;return 0;
}

右值对象构造,只有拷⻉构造,没有移动构造的场景

  • 图1展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次拷⻉构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次拷⻉构造。
  • 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰。
  • linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次拷⻉。

右值对象构造,有拷⻉构造,也有移动构造的场景

  • 图2展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次移动构造。
  • 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰。
  • linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次移动。

图二

图三

右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景

  • 图4左边展⽰了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次拷⻉构造,⼀次拷⻉赋值。
  • 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景

  • 图5左边展⽰了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次移动构造,⼀次移动赋值。
  • 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

右值引⽤和移动语义在传参中的提效

STL 容器中的右值引用:

在 STL 中,许多容器(如 std::liststd::vector 等)增加了支持右值引用的接口:

  • 当传入一个左值时,容器会调用拷贝构造函数。
  • 当传入一个右值时,容器会调用移动构造函数,将右值的资源swap到当前对象上。
// void push_back (const value_type& val);
// void push_back (value_type&& val);
// iterator insert (const_iterator position, value_type&& val);
// iterator insert (const_iterator position, const value_type& val);
int main()
{std::list<bit::string> lt;bit::string s1("111111111111111111111");lt.push_back(s1);cout << "*************************" << endl;lt.push_back(bit::string("22222222222222222222222222222"));cout << "*************************" << endl;lt.push_back("3333333333333333333333333333");cout << "*************************" << endl;lt.push_back(move(s1));cout << "*************************" << endl;return 0;
}运⾏结果:
string(char* str)
string(const string& s) --拷⻉构造
*************************
string(char* str)
string(string && s) --移动构造
~string() --析构
*************************
string(char* str)
string(string && s) --移动构造
~string() --析构
*************************
string(string && s) --移动构造
*************************
~string() --析构
~string() --析构
~string() --析构
~string() --析构
~string() --析构

类型分类

在C++中,类型分类是一个重要的概念,它决定了对象的生命周期、存储方式以及它们在表达式中的行为。C++11标准引入了新的类型分类,以支持右值引用和移动语义。

左值(Lvalue)

左值是指具有明确存储位置的对象,它们通常代表对象的身份。左值可以出现在赋值操作的左右两边,并且可以取地址。

特征:

  • 可以被赋值。
  • 可以取地址。
  • 代表对象的身份。

例子:

int a = 10; // 'a' 是一个左值,因为它有一个持久的存储位置。
int* p = &a; // 取'a'的地址,'p'现在指向'a'的存储位置。
a = 20; // 'a' 可以被赋值。

右值(Rvalue)

右值是指那些没有持久存储位置的对象,通常是临时的,比如字面量、表达式的计算结果等。右值代表的是值本身,而不是值所在的内存位置。

特征:

  • 不能被赋值。
  • 不能取地址。
  • 代表值本身。

例子:

int b = 30; // 'b' 是一个左值。
int c = b * 2; // 'b * 2' 是一个右值,因为它是一个表达式的计算结果。

纯右值(Prvalue)

C++11中引入了纯右值的概念,它指的是那些字面量常量或求值结果相当于字面量或是一个个不具名的临时对象。

特征:

  • 通常是临时对象或字面量。
  • 不能被移动。

例子:

int x = 42; // '42' 是一个纯右值。
int y = x + 2; // 'x + 2' 也是一个纯右值。

将亡值(Xvalue)

将亡值是指那些即将被移动的对象,它们通常是通过右值引用返回的函数调用表达式或转换为右值引用的转换函数的调用表达。

特征:

  • 可以被移动。
  • 代表即将被移动的对象。

例子:

int&& func() {int a = 10;return std::move(a);
}
int&& x = func(); // 'func()' 返回的是一个将亡值。

泛左值(Glvalue)

泛左值是C++11中引入的一个更广泛的概念,它包括了左值和将亡值。泛左值可以表示对象的身份,并且可以被取地址。

特征:

  • 包含左值和将亡值。
  • 可以被取地址。

例子:

int a = 10; // 'a' 是一个泛左值,因为它是一个左值。
int&& b = std::move(a); // 'b' 也是一个泛左值,因为它是一个将亡值。

引用折叠

什么是引用折叠?

引用折叠指的是当我们使用模板和类型别名(typedef)时,组合不同类型的引用会产生新的引用类型。C++11 规定了一些折叠规则来处理这些情况:

  • 右值引用的右值引用折叠成右值引用
  • 所有其他组合(如左值引用与右值引用、左值引用与左值引用等)都折叠成左值引用

为什么需要引用折叠?

在 C++ 中,引用的作用是为了避免不必要的拷贝,直接操作原对象。引用折叠使得在模板中使用引用时,能根据实际传入的参数类型自动决定使用左值引用还是右值引用,从而提高性能。

引用折叠的应用示例

函数模板

在函数模板中,T&& 是一种万能引用(或转发引用),根据传入的参数类型,**T**** 会推导为左值引用或右值引用**。如下所示:

  • f1(T& x) 总是实例化为左值引用,因为无论传入的是左值还是右值,T& 都不发生变化。
  • f2(T&& x) 根据传入的参数类型,实例化为左值引用或右值引用。例如,传入 int& 时,f2 实例化为 void f2(int& x);传入 int 时,实例化为 void f2(int&& x)
// 由于引用折叠规则,f1模板实例化后总是一个左值引用
template<class T>
void f1(T& x)
{}// 由于引用折叠规则,f2模板实例化后可以是左值引用或右值引用
template<class T>
void f2(T&& x)
{}// 没有折叠,实例化为 void f1(int& x)
// n 是左值,绑定到 T 的左值引用(即 T=int),故 f1<int>(n) 成功
f1<int>(n);  // 报错:0 是右值,不能绑定到左值引用
f1<int>(0); // 报错// 折叠,实例化为 void f1(int& x)
// n 是左值,T 推导为 int&,故实例化成功
f1<int&>(n);  // 报错:0 是右值,不能绑定到左值引用
f1<int&>(0); // 报错// 折叠,实例化为 void f1(int& x)
// n 是左值,T 推导为 int&&,因此实例化为左值引用
f1<int&&>(n); // 报错: 左值不能绑定到右值引用// 报错:0 是右值,不能绑定到左值引用
f1<int&&>(0); // 报错// 折叠,实例化为 void f1(const int& x)
// n 是左值,T 推导为 const int&,故实例化成功
f1<const int&>(n);  // 报错:0 是右值,不能绑定到 const 左值引用
f1<const int&>(0); // 报错// 折叠,实例化为 void f1(const int& x)
// n 是左值,T 推导为 const int&&,因为 const 的左值引用会折叠成左值引用
f1<const int&&>(n); // 报错:0 是右值,不能绑定到 const 左值引用
f1<const int&&>(0); // 报错// 没有折叠,实例化为 void f2(int&& x)
// n 是左值,不能绑定到右值引用,因此报错
f2<int>(n); // 报错// 报错:0 是右值,无法绑定到右值引用
f2<int>(0); // 报错// 折叠,实例化为 void f2(int& x)
// n 是左值,T 推导为 int&,所以实例化成功
f2<int&>(n);  // 报错:0 是右值,无法绑定到左值引用
f2<int&>(0); // 报错// 折叠,实例化为 void f2(int&& x)
// n 是左值,不能绑定到右值引用,因此报错
f2<int&&>(n); // 报错// 报错:0 是右值,能够绑定到右值引用,因此实例化成功
f2<int&&>(0); // 报错

示例2:

template<class T>
void Function(T&& t) // T 是万能引用(转发引用),会根据实参推导类型
{int a = 0;          // 定义一个整数 aT x = a;           // x 的类型根据 T 的推导结果而定// x++ 可能会报错,因为 x 的类型可能是 const 引用cout << &a << endl; // 输出 a 的地址cout << &x << endl; // 输出 x 的地址cout << endl;
}int main()
{// 10 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)Function(10); // 右值int a; // 定义一个整数 a// a 是左值,推导出 T 为 int&,引用折叠,模板实例化为 void Function(int& t)Function(a); // 左值// std::move(a) 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)Function(std::move(a)); // 右值const int b = 8; // 定义一个常量整数 b// b 是 const 左值,推导出 T 为 const int&,引用折叠,模板实例化为 void Function(const int& t)// 因为 t 是 const 引用,x 也会是 const 引用,因此 x++ 会报错Function(b); // const 左值// std::move(b) 是右值,推导出 T 为 const int,模板实例化为 void Function(const int&& t)// 因为 t 是 const 引用,x 也会是 const 引用,因此 x++ 会报错Function(std::move(b)); // const 右值return 0;
}

typedef 引用折叠

typedefusing 中定义的引用类型同样遵循引用折叠规则。lrefrref 的实例化表现如下:

  • lref&lref&& 都会折叠成 int&,即左值引用。
  • rref& 报错,因为它是引用的引用,最终折叠为左值引用。
typedef int& lref;   // lref = int&
typedef int&& rref;  // rref = int&&lref& r1 = n;  // OK: r1 是 int&
lref&& r2 = n; // OK: r2 是 int&
rref& r3 = n;  // 报错: rref 是 int&&,不能绑定到 int&
rref&& r4 = 1; // OK: r4 是 int&&

完美转发完美转发:保持函数参数的值类别

在 C++ 中,完美转发允许我们在模板中准确地转发参数的值类别(左值或右值)。通过完美转发,我们可以确保在函数内部调用其他函数时,参数的性质(左值或右值)不会丢失。

完美转发的背景

考虑以下情况:

  • 我们定义了一个函数模板 <font style="color:rgb(31,35,41);">Function</font>,它接受一个参数 <font style="color:rgb(31,35,41);">T&& t</font>。当传入一个右值时,<font style="color:rgb(31,35,41);">T</font> 会被推导为一个右值引用类型,而当传入一个左值时,<font style="color:rgb(31,35,41);">T</font> 会被推导为左值引用类型。
  • 然而,在函数内部,<font style="color:rgb(31,35,41);">t</font> 的值类别是左值,这会导致在调用另一个函数时只会匹配左值引用版本的函数。

为了保持参数的值类别,我们需要使用 <font style="color:rgb(31,35,41);">std::forward</font> 函数进行完美转发。

<font style="color:rgb(31,35,41);">std::forward</font> 的实现

<font style="color:rgb(31,35,41);">std::forward</font> 是一个函数模板,定义如下:

template <class T>
T&& forward(typename remove_reference<T>::type& arg) noexcept;template <class T>
T&& forward(typename remove_reference<T>::type&& arg) noexcept;

它的作用是将参数转换为其原始的值类别。<font style="color:rgb(31,35,41);">remove_reference</font> 用于移除引用,确保我们能够正确处理参数类型。

示例代码分析

让我们通过一个代码示例来理解完美转发的实现和使用。

#include <iostream>
using namespace std;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<class T>
void Function(T&& t) {// 这里直接传递 t,会导致 t 的值类别变为左值// Fun(t);// 使用 std::forward 保持 t 的原始值类别Fun(std::forward<T>(t));
}int main() {Function(10); // 右值int a;Function(a); // 左值Function(std::move(a)); // 右值const int b = 8;Function(b); // const 左值Function(std::move(b)); // const 右值return 0;
}

流程分析

  1. 右值传递
Function(10); // 右值
  • <font style="color:rgb(31,35,41);">T</font> 被推导为 <font style="color:rgb(31,35,41);">int</font>,所以 <font style="color:rgb(31,35,41);">Function</font> 实例化为 <font style="color:rgb(31,35,41);">void Function(int&& t)</font>,在<font style="color:rgb(31,35,41);">Function</font><font style="color:rgb(31,35,41);">t</font>变为了左值。
  • 使用<font style="color:rgb(31,35,41);">std::forward<T>(t)</font><font style="color:rgb(31,35,41);">t</font> 作为右值转发给 <font style="color:rgb(31,35,41);">Fun</font>,匹配 <font style="color:rgb(31,35,41);">Fun(int&& x)</font>
  1. 左值传递
int a;
Function(a); // 左值
  • <font style="color:rgb(31,35,41);">T</font> 被推导为 <font style="color:rgb(31,35,41);">int&</font>,实例化为 <font style="color:rgb(31,35,41);">void Function(int& t)</font>
  • 使用 <font style="color:rgb(31,35,41);">std::forward<T>(t)</font><font style="color:rgb(31,35,41);">t</font> 作为左值转发给 <font style="color:rgb(31,35,41);">Fun</font>,匹配 <font style="color:rgb(31,35,41);">Fun(int& x)</font>
  1. 使用 <font style="color:rgb(31,35,41);">std::move</font> 转发
Function(std::move(a)); // 右值
  • <font style="color:rgb(31,35,41);">std::move(a)</font><font style="color:rgb(31,35,41);">a</font> 转换为右值,<font style="color:rgb(31,35,41);">T</font> 被推导为 <font style="color:rgb(31,35,41);">int</font>
  • <font style="color:rgb(31,35,41);">std::forward<T>(t)</font><font style="color:rgb(31,35,41);">t</font> 作为右值转发,匹配 <font style="color:rgb(31,35,41);">Fun(int&& x)</font>
  1. 处理常量左值
const int b = 8;
Function(b); // const 左值
  • <font style="color:rgb(31,35,41);">T</font> 被推导为 <font style="color:rgb(31,35,41);">const int&</font>,实例化为 <font style="color:rgb(31,35,41);">void Function(const int& t)</font>
  • 转发时,匹配 <font style="color:rgb(31,35,41);">Fun(const int& x)</font>
  1. 处理常量右值
Function(std::move(b)); // const 右值
  • <font style="color:rgb(31,35,41);">std::move(b)</font><font style="color:rgb(31,35,41);">b</font> 转换为右值,<font style="color:rgb(31,35,41);">T</font> 被推导为 <font style="color:rgb(31,35,41);">const int</font>
  • 转发时,匹配 <font style="color:rgb(31,35,41);">Fun(const int&& x)</font>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/458452.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构——树、二叉树和森林间的转换

前言 介绍 &#x1f343;数据结构专区&#xff1a;数据结构 参考 该部分知识参考于《数据结构&#xff08;C语言版 第2版&#xff09;》129~130页 &#x1f308;每一个清晨&#xff0c;都是世界对你说的最温柔的早安&#xff1a;ૢ(≧▽≦)و✨ 目录 前言 1、基础知识 2…

Matlab 车牌识别技术

1.1设计内容及要求&#xff1a; 课题研究的主要内容是对数码相机拍摄的车牌&#xff0c;进行基于数字图像处理技术的车牌定位技术和车牌字符分割技术的研究与开发&#xff0c;涉及到图像预处理、车牌定位、倾斜校正、字符分割等方面的知识,总流程图如图1-1所示。 图1-1系统总…

《手写Spring渐进式源码实践》实践笔记(第十一章 AOP-基于JDK、Cglib实现对象动态代理)

文章目录 第十一章 基于JDK、Cglib实现对象动态代理背景目标设计实现代码结构类图代理案例解析案例代码运行结果拆解案例 实现步骤 测试事先准备自定义拦截方法测试用例测试结果&#xff1a; 总结 第十一章 基于JDK、Cglib实现对象动态代理 背景 到本章节我们将要从 IOC 的实现…

今日头条APP移动手机端留痕脚本

这两个的脚本目的是什么呢&#xff1f; 很简单&#xff0c;就是批量访问指定用户的首页&#xff0c;在他人访客记录里面留下你的账户信息&#xff0c;可以让对方访问你的头条&#xff0c;概率下会关注你的头条&#xff0c;目的嘛&#xff0c;这个自己细想&#xff01; 第1个是…

网页上的视频怎么下载下来?三种方法

分享三个简单好用的网页视频下载工具&#xff0c;值得使用&#xff01; 1.IDM IDM 是一款可以提高下载速度达5倍的工具&#xff0c;同时具有恢复、调度和组织下载的功能。如果由于网络问题或意外的电源中断&#xff0c;程序将恢复未完成的下载。 IDM 还具有一个完全功能的站点…

张驰咨询:六西格玛培训费用,到底值不值得花?

六西格玛作为一种先进的管理理念和统计方法&#xff0c;已经在全球范围内得到了广泛的应用和认可。它旨在通过减少流程变异&#xff0c;提高产品质量和客户满意度&#xff0c;从而为企业带来持续的改进和盈利增长。随着六西格玛理念的普及&#xff0c;越来越多的人和企业开始寻…

spark on kubernetes运行测试

测试环境 ● kubernetes 1.20.15 ● default命名空间 ● spark 3.1.2 ● kubectl 运行架构 构建镜像 配置JAVA_HOME下载spark二进制包spark-3.1.2-bin-hadoop3.2.tgz并解压修改kubernetes/dockerfiles/spark/Dockerfile文件 ARG java_image_tag11-jre-slimFROM openjdk:${j…

HBuilder X 中Vue.js基础使用2(三)

一、条件渲染 1、条件判断 v-if &#xff1a; 表达式返回真值时才被渲染 v-else &#xff1a;表达式返回为假时不被渲染 2、 分支条件判断 v-else-if &#xff1a;使用v-if , v-else-if 和 v-else 来表示其他的条件分支 3、显示隐藏 v-show v-show true 把节点显示 …

持续深化信创布局,途普科技与统信软件完成产品兼容性互认证

近日&#xff0c;由北京途普科技有限公司&#xff08;以下简称“途普科技”&#xff09;自主研发的TopGraph图数据库及知识图谱构建平台已成功完成统信服务器操作系统V20的兼容性互认证&#xff0c;标志着途普科技在国产自控技术上又迈出了坚实的一步。 在各项严格的测试环节中…

技术成神之路:设计模式(二十一)外观模式

相关文章&#xff1a;技术成神之路&#xff1a;二十三种设计模式(导航页) 介绍 外观模式&#xff08;Facade Pattern&#xff09;是一种结构型设计模式&#xff0c;它为子系统中的一组接口提供一个统一的接口。外观模式定义了一个高层接口&#xff0c;使得子系统更容易使用。 …

XJ02、消费金融|消费金融业务模式中的主要主体

根据所持有牌照类型的不同,消费金融服务供给方主要分为商业银行、汽车金融公司、消费金融公司和小贷公司,不同类型机构定位不同、提供消费金融服务与产品类型也各不相同。此外,互联网金融平台也成为中国消费金融业务最重要的参与方之一,虽其并非持牌金融机构,但借助其流量…

D50【python 接口自动化学习】- python基础之类

day50 init方法 学习日期&#xff1a;20241027 学习目标&#xff1a;类 -- 64 init方法&#xff1a;如何为对象传递参数&#xff1f; 学习笔记&#xff1a; 魔术方法 init方法 class Klass(object):# 定义初始化方法&#xff0c;类实例化时自动进行初始化def __init__(self…

AGI 之 【Dify】 之 Dify 在 Windows 端本地部署调用 Ollama 本地下载的大模型,实现 API 形式进行聊天对话

AGI 之 【Dify】 之 Dify 在 Windows 端本地部署调用 Ollama 本地下载的大模型&#xff0c;实现 API 形式进行聊天对话 目录 AGI 之 【Dify】 之 Dify 在 Windows 端本地部署调用 Ollama 本地下载的大模型&#xff0c;实现 API 形式进行聊天对话 一、简单介绍 二、创建一个聊…

基于SSM+小程序的旅游社交登录管理系统(旅游4)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 ​ 本旅游社交小程序功能有管理员和用户。管理员有个人中心&#xff0c;用户管理&#xff0c;每日签到管理&#xff0c;景点推荐管理&#xff0c;景点分类管理&#xff0c;防疫查询管理&a…

洞察前沿趋势!2024深圳国际金融科技大赛——西丽湖金融科技大学生挑战赛技术公开课指南

在当前信息技术与“互联网”深度融合的背景下&#xff0c;金融行业的转型升级是热门话题&#xff0c;创新与发展成为金融科技主旋律。随着区块链技术、人工智能技术、5G通信技术、大数据技术等前沿科技的飞速发展&#xff0c;它们与金融领域的深度融合&#xff0c;正引领着新型…

Golang 怎么高效处理ACM模式输入输出

文章目录 问题bufio.NewReader高效的原理 再次提交 问题 最近在练习牛客上单调栈题目时&#xff0c;要求自己处理出入输出&#xff0c;也就是读题库要求的输入&#xff0c;计算最终结果&#xff0c;并打印输出 当我用fmt.Scan处理输入&#xff0c;用fmt.Println处理输出时&am…

R语言笔记(五):Apply函数

文章目录 一、Apply Family二、apply(): rows or columns of a matrix or data frame三、Applying a custom function四、Applying a custom function "on-the-fly"五、Applying a function that takes extra arguments六、Whats the return argument?七、Optimized…

linux开机自启动三种方式

方式一、 1&#xff1a;rc.local 文件 1、执行命令&#xff1a;编辑 “/etc/rc.local” vi /ect/rc.local 2、然后在文件最后一行添加要执行程序的全路径。 例如&#xff0c;每次开机时要执行一个 hello.sh&#xff0c;这个脚本放在 / usr 下面&#xff0c;那就可以在 “/et…

深入了解 Android 中的命名空间:`xmlns:tools` 和其他常见命名空间

在 Android 开发中&#xff0c;xmlns &#xff08;.xml的namespace&#xff09;命名空间是一个非常重要的概念。通过引入不同的命名空间&#xff0c;可以使用不同的属性来设计布局、设置工具属性或者支持自定义视图等。除了 xmlns:tools 以外&#xff0c;还有很多常见的命名空间…

动态IP是什么?

随着互联网成为人们生活的重要组成部分&#xff0c;以信息传递为主导的时代种&#xff0c;网络连接质量对我们的工作效率、学习进度以及娱乐体验等方面都有很大影响。 动态IP&#xff0c;作为网络连接中的一种重要IP代理形式&#xff0c;越来越受到用户的欢迎。本文将深入解析…