首页 > 编程语言 >[C++11] 右值引⽤与移动语义

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

时间:2024-10-27 21:45:54浏览次数:6  
标签:11 const string 右值 int 左值 C++ 引用

文章目录


左值和右值

在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;          // 定义一个整数 a
    T 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>

标签:11,const,string,右值,int,左值,C++,引用
From: https://blog.csdn.net/SDFsoul/article/details/143275441

相关文章

  • Complete the Sequence 第一次做英文c++的题
    第一次接触全是英语的题,怎么会有这么难的呢?首先我拿起了它和中文的题目一对比,发现分成了5个板块,将这5个板块细细拆分后,了解到了大意,大意为输入n组数据,其中输入x个数,然后找出它的规律,输出接下来的y个数。比如一组数据,1、2、3、4、5、6,要输出剩下的数据,你肯定会不有毫不犹豫的回答......
  • C++统计资源消耗和耗时
    在Linux下统计可执行程序的执行耗时,你可以使用 time 命令。它能够显示程序的用户时间、系统时间和总耗时等详细信息。以下是 time 命令的基本使用方式:time./your_program其中 your_program 是你要运行的可执行文件。输出解释:real0m2.123s#实际的时间(从程序开......
  • 2024.10.27~2024.11.3
    2024.10.27这么说吧,csp-s打的不好,是时候做出些调整了约法n章:1.在NOIP之前把ybt刷完,保守估计一天5道题2.一道题若超出一个半小时内没有A就换下一道题,并在博客中记录此题并整理思路,有时间补完3.模拟赛我的得分要有以下两种评估:切题得分和难题高分暴力得分4.禁用一个月B站,休息......
  • 2.11(学号:3025)
    importnumpyasnpdeff(x):return(abs(x+1)-abs(x-1))/2+np.sin(x)defg(x):return(abs(x+3)-abs(x-3))/2+np.cos(x)fromscipy.optimizeimportfsolvedefequation_system(vars):x1,x2,y1,y2=varseq1=2x1-3f(y1)-4g(y2)+1eq2......
  • C++ 模板编程:解锁高效编程的神秘密码
     快来参与讨论......
  • P11233 CSP-S 2024 染色
    P11233CSP-S2024染色考试最后码方程忘记\(a[i-1]\)了,调不出来,只好\(50pts\)收尾。思路\(dp\)的难点在于确定一段的颜色后,无法快速找到上一段相同颜色的结尾。从这里入手,设\(dp[i][0/1][0/1]\)表示第\(i\)位颜色为\(1/0\),第三维表示是一段颜色的\(0\)开头或\(1......
  • DBAP011 Business Analytics
    DBAP011BusinessAnalyticsQualityDiagnosisattheKelpiesFurnitureScotlandDUEDATEThursday31st October2024,11amIntroductionTheKelpiesFurnitureScotland(KFS)isafamilybusinessspecializinginthedesignandmanufactureofcontemporaryfu......
  • 【C++设计模式】之单例模式,一文看懂
    【C++设计模式】之单例模式一、什么是单例模式二、单例模式的实现1.懒汉版单例模式懒汉单例模式代码实现2.饿汉单例模式饿汉单例模式代码实现3.线程安全的懒汉单例模式线程安全的懒汉模式代码实现三、总结一、什么是单例模式单例模式是一种创建型设计模式,它的......
  • 探索C++的奥秘之C/C++内存管理
    一个程序当中的数据主要有以下几类:局部数据、静态数据和全局数据、常量数据、动态申请数据。1.C/C++内存分布1.栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。2.内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系......
  • 编译器的实现是用C好还是C++
    标题:编译器的实现是用C好还是C++在探讨编译器的实现时,是否选择C语言或C++取决于多个因素,包括性能需求、团队熟悉度、以及项目的特定需求。C语言以其运行时性能和底层操作的直接性著称,是许多传统编译器如GCC的选择。C++提供了更高级的抽象和面向对象的特性,使得大型项目的组织和维......