[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.mzph.cn/diannao/58057.shtml

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

相关文章

传输层UDP

再谈端口号 端口号&#xff1a;标识了主机上进行通信的不同的应用程序 在TCP/IP 协议中我们用“源IP”"源端口号" “目的IP”“目的端口号” “协议号”五元组来标识一个通信 用netstat -n 查看 查看网络信息&#xff0c;我们有两种命令查看网络通信1.用netsta…

Linux-练习3

题目&#xff1a; 操作过程&#xff1a; 1.建立用户组 shengcan&#xff0c;其id 为 2000 2.建立用户组 caiwu&#xff0c;其id 为 2001 3.建立用户组 jishu&#xff0c;其 id 为 2002 4.建立用户 lee&#xff0c;指定其主组 id 为 shengchan&#xff0c;附加组为 jishu 和…

多GPU训练大语言模型,DDP, ZeRO 和 FSDP

在某些时候&#xff0c;我们可能需要将模型训练扩展到单个 GPU 之外。当模型变得太大无法适应单个 GPU 时&#xff0c;需要使用多 GPU 计算策略。但即使模型适合单个 GPU&#xff0c;使用多个 GPU 来加速训练也是有好处的。即使您正在处理一个小模型&#xff0c;了解如何在多个…

在浏览器中运行 Puppeteer:解锁新能力

Puppeteer&#xff0c;这个强大的浏览器自动化工具&#xff0c;通常在Node.js环境中运行。但你有没有想过&#xff0c;在浏览器本身中运行Puppeteer会是什么样子&#xff1f;这不仅能让我们利用Puppeteer的功能完成更多任务&#xff0c;还能避开Node.js特定的限制。 支持的功…

【Canvas与桌面】文山甲密铺桌面壁纸 1920*1080

【成图】 不加蒙版的部分截图&#xff1a; 加上蒙版的桌面壁纸图&#xff1a; 不加蒙版的桌面壁纸图&#xff1a; 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8&qu…

ts:对象数组的简单使用

ts中对象数组的简单使用 一、主要内容说明二、例子1、源码12、源码1运行效果 三、结语四、定位日期 一、主要内容说明 平常ts创建数组的格式如下&#xff1a; let array:string[]["元素1","元素2","元素3","元素3","元素4"…

Java语言-异常

目录 1.异常的概念与体系结构 1.1 异常的概念 1.2 异常的体系结构 1.3 异常的分类 1.3.1 编译时异常(受查异常) 1.3.2 运行时异常(非受查异常) 2.异常的处理 2.1 防御式编程 2.1.1 LBYL 2.1.2 EAFP 2.2 异常的抛出 2.3 异常的捕获 2.3.1 异常声明throws 2.3.2 …

Spring IoC DI

博主主页: 码农派大星. 数据结构专栏:Java数据结构 数据库专栏:MySQL数据库 JavaEE专栏:JavaEE 关注博主带你了解更多数据结构知识 目录 1. 应用分层 1.1 如何分层: 1.2 MVC与三层架构区别联系 2. Spring 3.IoC & DI⼊⻔ 3.1 什么是IoC&#xff1f; 3.2 DI 介绍 …

ctfshow——web(持续更新)

文章目录 1、web签到题——base64编码特征2、web2——登录框测试&sqlmap使用3、web3——php伪协议 1、web签到题——base64编码特征 查看源代码&#xff1a; base64编码特征&#xff1a;大小写数字&#xff0c;偶尔最后几位是。 2、web2——登录框测试&sqlmap使用 …

合约门合同全生命周期管理系统:企业智能合同管理的新时代

合约门合同全生命周期管理系统&#xff1a;企业智能合同管理的新时代 1. 引言 随着现代企业的快速发展&#xff0c;合同管理的复杂性日益增加。无论是采购合同、销售合同还是合作协议&#xff0c;合同管理已成为企业运营中至关重要的一环。传统的手工合同管理方式往往效率低下…

解读AVL树:平衡二叉搜索树的奥秘

✨✨小新课堂开课了&#xff0c;欢迎欢迎~✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;C&#xff1a;由浅入深篇 小新的主页&#xff1a;编程版小新-CSDN博客 前言&#xff1a; 前面我们已经介绍了二叉搜…

交易逆序对的总数 ---- 分治-归并

题目链接 题目: 分析: 解法一: 暴力解法, 遍历所有的数对, 找到逆序对, 需要两重for循环, 一定会超时解法二: 归并排序的思想如果我们将数组分成两半, 我们在每一半中各找一个数字, 判断是否为逆序对, 再根据归并的思想, 再将一半数组分半, 判断是否为逆序对, 直到数组只有一…

【论文笔记】xGen-MM (BLIP-3): A Family of Open Large Multimodal Models

&#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&#xff0c;为生民立命&#xff0c;为往圣继绝学&#xff0c;为万世开太平。 基本信息 标题: xGen-MM (BLIP-3): A Fami…

网络编程_day3

#1024程序员节 #三次握手四次挥手#四次挥手#udp#recvfrom#sendto#服务器模型#客户端模型#Linux IO模型#阻塞式IO#非阻塞IO#设置非阻塞的方式 目录 【0】复习 【1】三次握手四次挥手 四次挥手 四次挥手既可以由客户端发起&#xff0c;也可以由服务器发起 【2】udp 1. 通信流程 2…

实验:使用Oxygen发布大型手册到Word格式

此前&#xff0c;我曾发表过一篇文章《结构化文档发布的故事和性能调优》&#xff0c;文中讨论了在将大型DITA手册转换为PDF格式时可能遇到的性能挑战及相应的优化策略。 近日&#xff0c;有朋友咨询&#xff0c;若将同样的大型手册输出为MS Word格式&#xff0c;是否也会面临…

Linux复习-C++

参考博客&#xff1a; https://blog.csdn.net/qq_45254369/article/details/126023482?ops_request_misc%257B%2522request%255Fid%2522%253A%252277629891-A0F3-4EFC-B1AC-410093596085%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&req…

[JAVAEE] 多线程的案例(一)-单例模式

目录 一. 单例模式 二. 单例模式的使用时机 三. 单例模式的关键代码 四. 单例模式的几种实现方式 4.1 饿汉方式(急) 4.2 懒汉模式(缓) a. 解决原子性的问题 b. 解决程序运行效率低下的问题 c. 解决指令重排序的问题(其次是为了解决内存可见性的问题) 五. 总结 一. …

HCIP-HarmonyOS Application Developer 习题(十七)

&#xff08;判断&#xff09;1、对于用户创建的一些临时卡片在遇到卡片服务框架死亡重启&#xff0c;此时临时卡片数据在卡片管理服务中已经删除&#xff0c;且对应的卡片ID不会通知到提供方&#xff0c;所以卡片使用方需要自己负责清理长时间未刚除的临时卡片数据。 答案&…

QT:MaintenanceTool 模块安装工具

QT的MaintenanceTool 工具对已安装的 Qt 进行卸载、修复等其他操作时提示At least one valid and enabled repository required for this action to succeed 解决方式&#xff1a;在设置中添加一个临时的仓库 https://mirrors.tuna.tsinghua.edu.cn/qt/online/qtsdkrepositor…

6,000 个网站上的假 WordPress 插件提示用户安装恶意软件

黑客使用窃取的凭证感染 WordPress 网站&#xff0c;并向其发送虚假插件&#xff0c;通过虚假的浏览器更新提示向最终用户发送恶意软件和信息窃取程序。 该恶意活动基于ClickFix假浏览器更新恶意软件的新变种&#xff0c;自 2024 年 6 月以来已使用假 WordPress 插件感染了超过…