【C++】移动语义和完美转发
<h2 id="前言">前言</h2><p>学习C++移动语义和完美转发笔记,记录左值、右值、std::move()、万能引用、引用折叠等相关内容。</p>
<h2 id="概念">概念</h2>
<ul>
<li>
<p><strong>左值 (lvalue)</strong> 它是<strong>在内存中有明确存储地址、可以被寻址</strong>的值。如果你可以对一个表达式取地址(使用 <code>&</code> 运算符),那么它就是一个左值。左值通常是持久的,在它所在的定义域结束之前一直存在</p>
</li>
<li>
<p><strong>左值引用(Lvalue Reference)本质上就是给一个现有的左值起了一个“别名”</strong>,左值引用定义即初始化。</p>
<ul>
<li><strong>普通左值引用 (<code>T&</code>)</strong>:只能绑定到<strong>非 const 左值</strong>。</li>
<li><strong>常量左值引用 (<code>const T&</code>)</strong>:可以绑定到<strong>一切</strong>(左值、const 左值、右值)。</li>
</ul>
</li>
</ul>
<pre><code class="language-C++">const int& temp = 10;
//编译器会在内存中产生一个临时变量存储 10。
//temp 绑定到这个临时变量上。
//这个临时变量的寿命会变得和引用 temp 一样长。
</code></pre>
<ul>
<li>
<p><strong>右值 (rvalue)</strong> 右值就是那些<strong>临时出现、没有持久名字、无法取地址</strong>的值。如果你无法对一个表达式使用 <code>&</code> 取地址运算符,或者它是一个即将销毁的临时对象,它就是右值。右值通常是“瞬时”的,在包含它的表达式执行完之后,它就会被立即销毁.</p>
</li>
<li>
<p><strong>右值引用(Rvalue Reference)一种绑定到右值(临时对象)的引用类型</strong>。</p>
<blockquote>
<p>int&& rref = 10;</p>
</blockquote>
</li>
<li>
<p><code>std::move</code> 并不移动任何东西。它的唯一作用是:<strong>强制将一个左值转为右值引用</strong>。</p>
<pre><code class="language-C++">template <typename T>
typename std::remove_reference<T>::type&& move(T&& t) {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
</code></pre>
<ul>
<li>
<p><strong><code>std::remove_reference<T>::type</code></strong>:这是一个类型萃取工具。无论 <code>T</code> 是 <code>int</code>、<code>int&</code> 还是 <code>int&&</code>,它都能把修饰符去掉,只留下纯粹的底层类型 <code>int</code>。</p>
</li>
<li>
<p><code>static_cast</code> 将输入变量 <code>t</code> 强制转换为该类型的<strong>右值引用</strong>(即 <code>type&&</code>)</p>
</li>
</ul>
</li>
<li>
<p><strong>std::forward</strong> 如果原始参数是<strong>左值</strong>,转发后仍然是<strong>左值</strong>。如果原始参数是<strong>右值</strong>,转发后仍然是<strong>右值</strong>。</p>
</li>
</ul>
<pre><code class="language-C++">template <typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
</code></pre>
<ul>
<li>
<p><strong>移动语义(Move Semantics)</strong> 本质是<strong>资源所有权的转移</strong>,即将一个临时对象(右值)持有的资源转移,来避免昂贵的深拷贝操作。是由类实现的<strong>功能</strong>(通过移动构造函数)</p>
</li>
<li>
<p><strong>完美转发(Perfect Forwarding)</strong> 是:<strong>在函数模板中,将参数原封不动地转发给另一个函数,同时完全保留参数的所有属性(包括它是左值还是右值、是否带有 <code>const</code> 或 <code>volatile</code> 修饰符)。</strong></p>
<pre><code class="language-C++">template <typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
</code></pre>
<ul>
<li>
<p>如果原始参数是<strong>左值</strong>,转发后仍然是<strong>左值</strong>。</p>
</li>
<li>
<p>如果原始参数是<strong>右值</strong>,转发后仍然是<strong>右值</strong>。</p>
</li>
</ul>
</li>
<li>
<p><strong>万能引用</strong></p>
<ul>
<li>使用 <code>&&</code> 符号</li>
<li><strong>必须发生模板类型推导</strong>:通常出现在 <code>template <typename T></code> 之后的 <code>T&&</code>(不能加 <code>const</code>)。</li>
<li><strong>形式必须完全匹配</strong>:必须是具体的 <code>T&&</code>,不能有 <code>const</code> 或 <code>std::vector<T>&&</code> 等修饰。</li>
<li><strong>万能引用是完美转发的“门”。</strong> 它把参数原封不动地领进来(无论是左值还是右值),然后配合 <code>std::forward</code> 把它原封不动地送出去。</li>
</ul>
</li>
<li>
<p><strong>引用折叠</strong>是 C++ 编译器在处理“引用的引用”时遵循的一套自动简化规则。只要有左值引用(&)参与,结果就是左值引用;只有全是右值引用(&&)时,结果才是右值引用。</p>
</li>
</ul>
<h2 id="内容">内容</h2>
<h3 id="左值">左值</h3>
<p>内存中有明确存储地址、可以被寻址的值</p>
<pre><code class="language-C++">int a = 10; // a 是左值(有名字,可取地址)
a = 20; // a 在左边,OK
int* p = &a; // p 是左值
*p = 30; // *p(解引用结果)是左值
int** pp = &(*p);// 我们可以对 (*p) 再次取地址 ,可以看到*p是可以取地址的
const int b = 5; // b 是左值(虽然不可修改,但它有内存地址,是具名变量)
void func(int&& x) {
// 这里的 x 类型是右值引用,但 x 本身是一个有名字的变量
// 所以在函数内部,x 是一个左值!
int* p = &x; // 这是合法的
}
</code></pre>
<h3 id="左值引用">左值引用</h3>
<p>对左值的引用,即给左值起别名,必须初始化。</p>
<pre><code class="language-C++">int a = 10;
int& ref = a;// ref 是 a 的左值引用
const int& r3 = 10; // 正确!
std::cout << "a" << "b" << "c"; // 每次调用 << 都返回 cout 的左值引用
</code></pre>
<h3 id="右值">右值</h3>
<p>没有固定地址、没有名字的值,通常是临时结果、字面量或即将销毁的对象。</p>
<pre><code class="language-C++">10; // 纯右值:字面量
a + b; // 纯右值:运算的中间结果,没有名字
func(5); // 如果 func 返回一个值(非引用),func(5) 就是右值
std::string("Hi");// 纯右值:临时构造的匿名对象
</code></pre>
<h3 id="右值引用">右值引用</h3>
<p>绑定到右值上。 它的符号是 <code>&&</code></p>
<pre><code class="language-C++">int&& r1 = 10; // 正确:10 是右值
int a = 10;
int&& r3 = std::move(a); // 正确:std::move 把左值转成了右值(将亡值)
int n = 5;
// int&& r = ++n; // 错误:++n 返回的是修改后的 n 本身(有地址)
int&& r = n++; // 正确:n++ 返回的是一个临时副本(旧值 5),n 本身已变
</code></pre>
<h3 id="万能引用引用折叠">万能引用&引用折叠</h3>
<pre><code class="language-C++">template<typename T>
void func(T&& param); // 这是一个万能引用
//场景 A:传入左值变量
int a = 10;
func(a);
//因为 a 是左值,根据万能引用的特殊推导规则,编译器将 T 推导为 int&。
//为什么不能是int;因为如果为 int,函数变为 void func(int && param) 这个函数只能接收右值,所以编译器只能推导为int &
//代入模板:函数签名变成 void func(int& && param);。
//引用折叠:根据规则,int& && 含有左值引用,折叠为 int&。
//最终形态:void func(int& param); —— 成功以左值引用的方式接收了变量。
//场景 B:传入右值字面量 20
func(20);
//推导:因为 20 是右值,编译器将 T 推导为 int。
//模板:函数签名变成 void func(int&& param);。
//折叠:没有冲突,或者看作 int&&,保持 int&&。
//最终形态:void func(int&& param); —— 成功以右值引用的方式接收了临时变量
</code></pre>
<h3 id="移动语义">移动语义</h3>
<p>我们将一个临时对象赋值给另一个对象时,会触发<strong>深拷贝</strong>。比如一个包含 很多数据的 <code>std::vector</code>,拷贝它需要重新分配内存再复制数据,这非常耗时。</p>
<p><strong>移动语义</strong> 允许我们直接获取临时对象的资源,只需修改指针指向,而不必重新分配内存。</p>
<pre><code class="language-C++">#include <iostream>
#include <vector>
#include <utility>
class MyBuffer {
private:
int* data; // 唯一的私有属性
public:
// 构造函数 explicit防止隐式转换
//MyBuffer b2 = 100; ❌ 编译错误,不能隐式转换
explicit MyBuffer(int value) : data(new int(value)) {
std::cout << "分配内存并存入: " << *data << std::endl;
}
// 析构函数
~MyBuffer() {
if (data) {
delete data;
data = nullptr;
std::cout << "释放内存" << std::endl;
}
}
// ---------------------------------------------------------
// 拷贝构造函数 (Copy Constructor) - 深拷贝
// ---------------------------------------------------------
MyBuffer(const MyBuffer& other) : data(other.data ? new int(*other.data) : nullptr) {
std::cout << "深拷贝数据: " << (data ? *data : 0) << std::endl;
}
// ---------------------------------------------------------
// 移动构造函数 (Move Constructor)
// ---------------------------------------------------------
MyBuffer(MyBuffer&& other) noexcept : data(other.data) {
other.data = nullptr;
std::cout << "资源所有权已转移" << std::endl;
}
// ---------------------------------------------------------
// 移动赋值运算符 (Move Assignment Operator)
// ---------------------------------------------------------
MyBuffer& operator=(MyBuffer&& other) noexcept {
std::cout << "执行移动赋值" << std::endl;
if (this != &other) {
delete data; // 1. 释放当前对象持有的旧内存
data = other.data;// 2. 接管新资源
other.data = nullptr;
}
return *this;
}
// 访问器方法
int getValue() const {
return data ? *data : 0;
}
void setValue(int value) {
if (data) {
*data = value;
} else {
data = new int(value);
}
}
// 检查是否拥有资源
bool isEmpty() const {
return data == nullptr;
}
};
int main() {
std::cout << "=== 测试移动语义 ===" << std::endl;
// 测试移动构造函数
MyBuffer b1(100);
MyBuffer b2(std::move(b1)); // 触发移动构造
// b1 现在是"有效但未指定状态"
std::cout << "b1是否为空: " << b1.isEmpty() << std::endl;
std::cout << "b2的值: " << b2.getValue() << std::endl;
// 测试移动赋值
MyBuffer b3(300);
MyBuffer b4(400);
std::cout << "\n移动赋值前 - b3: " << b3.getValue() << ", b4: " << b4.getValue() << std::endl;
b4 = std::move(b3);
std::cout << "移动赋值后 - b3是否为空: " << b3.isEmpty() << std::endl;
std::cout << "移动赋值后 - b4: " << b4.getValue() << std::endl;
return 0;
}
</code></pre>
<p><strong>out</strong></p>
<pre><code class="language-bat">root1@ubuntu:~/work/hello/build$ ./hello_cmake_g
=== 测试移动语义 ===
分配内存并存入: 100
资源所有权已转移
b1是否为空: 1
b2的值: 100
分配内存并存入: 300
分配内存并存入: 400
移动赋值前 - b3: 300, b4: 400
执行移动赋值
移动赋值后 - b3是否为空: 1
移动赋值后 - b4: 300
释放内存
释放内存
</code></pre>
<h3 id="完美转发">完美转发</h3>
<pre><code>#include <iostream>
#include <utility>
void target(int& x) {
std::cout << "调用左值函数\n";
}
void target(int&& x) {
std::cout << "调用右值函数\n";
}
template <typename T>
void perfectForwarder(T&& arg) {
// std::forward 会根据 T 的类型决定是 cast 成左值还是右值
target(std::forward<T>(arg));
}
int main() {
int a = 10;
std::cout << "传递了右值\n";
perfectForwarder(a);
std::cout << "传递了右值\n";
perfectForwarder(20);
}
</code></pre>
<p><strong>out</strong></p>
<pre><code class="language-bat">root1@ubuntu:~/work/hello/build$ ./hello_cmake_g
传递了右值
调用左值函数
传递了右值
调用右值函数
</code></pre>
<h2 id="思考">思考</h2>
<hr>
<h3 id="i-和-i的区别为什么要习惯性写i-"><strong>++i</strong> 和 <strong>i++</strong>的区别,为什么要习惯性写++i ?</h3>
<ul>
<li>
<p>前置自增 <code>++i</code> (左值)</p>
<p>在 C++ 的底层实现中,前置自增的操作类似于:“先给这个内存地址里的值加 1,然后把这个<strong>地址</strong>传回去。”</p>
<ul>
<li>
<p><strong>返回类型:</strong> 通常是引用类型(如 <code>T&</code>)。</p>
</li>
<li>
<p><strong>内存逻辑:</strong> 它直接在原变量上操作,不产生中间人。</p>
</li>
<li>
<p><strong>为什么是左值:</strong> 因为它返回的是变量本身,它在表达式结束后依然存在,拥有确定的内存地址。</p>
</li>
</ul>
</li>
<li>
<p>后置自增 <code>i++</code> (右值)</p>
<p>后置自增的操作逻辑则复杂一些:“先把当前的值存到一个临时地方,给原变量加 1,然后把刚才那个<strong>临时值</strong>传回去。”</p>
<ul>
<li>
<p><strong>返回类型:</strong> 通常是按值返回(如 <code>T</code>)。</p>
</li>
<li>
<p><strong>内存逻辑:</strong> 产生了一个<strong>临时对象(Temporary Object)</strong>。</p>
<ul>
<li><strong>为什么是右值:</strong> 那个临时副本在表达式执行完的那一刻就被销毁了。它没有持久的“身份”,你无法通过地址再次找到它,因此它是右值。</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><strong>对于内置类型(如 int):</strong> 现代编译器非常聪明,通常会把 <code>i++</code> 优化掉,使两者性能一致。</p>
<p><strong>对于自定义类型(如迭代器 <code>std::map::iterator</code>):</strong> 区别极大。</p>
<ul>
<li><code>++i</code> 直接修改内部指针并返回引用。</li>
<li><code>i++</code> 必须先调用拷贝构造函数创建一个<strong>副本</strong>,修改原值,最后返回那个副本。这个副本的创建和随后的析构都是额外的开销。</li>
</ul>
<hr>
<hr>
<h3 id="stdmove-与-stdforward-的本质区别">std::move 与 std::forward 的本质区别?</h3>
<ul>
<li><strong><code>std::move<T>(x)</code></strong>:<strong>无条件转换</strong>。不管 <code>x</code> 是什么,通通强制转为右值引用。</li>
<li><strong><code>std::forward<T>(x)</code></strong>:<strong>有条件转换</strong>。只有当 <code>T</code> 被推导为右值引用时,才将其转换为右值引用;否则保持左值属性。</li>
</ul>
<hr>
<hr>
<h3 id="移动构造函数为什么要加-noexcept">移动构造函数为什么要加 <code>noexcept</code>?</h3>
<p><strong>为了 STL 容器的异常安全性。</strong> 当 <code>std::vector</code> 扩容需要搬移元素时,如果你的移动构造函数不声明 <code>noexcept</code>,<code>vector</code> 为了保证在搬移失败时能回滚,会放弃使用高效的“移动语义”,转而使用效率较低的“拷贝构造”。</p>
<hr><br><br>
来源:https://www.cnblogs.com/hjk-airl/p/19434573
頁:
[1]