%%
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
%%
# 引用
C++ 中的**引用类型**(reference type)是一种复合类型,以 `typyname&` 表示(左值引用)。
"引用" 实际上是**对象的别名**,其**永远指向最初引用的对象**:
- "**引用**" **绑定 (bind)到某一类型的原始变量**,并**用作原始变量的"==别名=="**。
- "**引用**" **==必须在创建时被初始化==**。一旦完成创建,引用在其**生命周期内**将始终**与其初始值对象绑定**。
- 作为 "类数据成员" 时,必须**在 "列表初始化" 中**完成初始化。
- "引用" 本身**并非一个可独立寻址的对象实体**,故相较于指针不会带来额外开销,也不存在 "空引用"。
> [!caution] 与指针相比,引用不会带来**额外开销**。
>
> **"引用"本身不是一个可独立寻址(即具有自己地址)的对象实体,而是绑定到另一个对象上的别名**。
> 因此,**C++中不存在 "==引用的引用=="、 "指向引用的指针"**、**"引用数组"**。
```cpp title:reference_exam.cpp
int x {3}, y {4};
int& x_ref {x}; // 引用变量`x_ref`绑定到x, 作为`x`的别名
x_ref = y; // 将x的值修改为y的值
```
> [!FAQ] ❓ 引用类型占用的内存空间
>
> - 引用类型作为 "**普通变量**" 时,通常不占内存空间,其**作为被引用对象的别名**,**编译器会将==对 "引用" 的使用==直接替换为对 "被引用对象" 的使用。**
> - 引用类型作为 "**类数据成员**" 时,**通常==等同于一个指针==**,**占有==与指针相同的内存大小==**,因为**其需要存储被引用对象的地址**,从而保持运行时的追踪。
>
> 说明示例:
>
> ```cpp
> struct MyClass {
> MyClass(int& v) : rb(v) {}
> int& rb;
> };
>
> struct MyClass2 {
> MyClass2(int&v) {}
> };
>
> int main() {
> int a = 15;
> MyClass obj(a);
> MyClass2 obj2(a);
> cout << sizeof(obj) << endl; // 8字节
> cout << sizeof(obj2) << endl; // 1字节
> return 0;
> }
> ```
>
<br><br>
## 引用的生命周期与有效性
引用的「**生命周期**」受其**作用域**的限制,起始于作用域的开始,并在退出作用域时结束。
「**引用**」本质上是**被引用对象的别名**,因此引用本身没有独立的存储,其「**有效性**」绑定于 "**==所引用的对象的生命周期==**"。
**一旦==所引用对象的生命周期结束==,对象的内存被销毁/释放,引用即失效**,成为 "**==悬挂引用==**"。
<br><br>
## 悬挂引用
**悬挂引用**(dangling reference)是指 "**指向已经被销毁或释放内存的引用**"。类似于 "**悬挂指针**"。
即「**引用**」仍然存在,但其**所引用的对象==已不再有效==**,因此**再通过该引用访问数据会导致未定义行为**。
#### 悬挂引用产生的场景
###### 函数返回对局部变量的引用
函数结束后局部变量内存被释放, 因此引用成了悬挂引用
> [!danger] 避免悬挂引用:禁止返回对局部变量的引用!
```cpp
int& func() {
int a = 5;
return a; // 返回了对函数内局部变量的引用, 函数结束后局部变量内存被释放, 因此引用成了悬挂引用
}
```
###### **指向已被释放的动态内存的引用**
> [!caution] 避免悬挂引用:**使用==智能指针==管理动态分配的内存**,而不是引用!
>
> 如 `std::unique_ptr` 或 `std::shared_ptr`,这些智能指针会自动管理内存生命周期,减少内存泄漏和悬挂引用的风险。
```cpp
int* ptr = new int(10);
int& ref = *ptr;
delete ptr; // 此后, ref成为了"悬空引用"
```
###### **指向容器元素的引用**
- 向容器中添加元素可能导致**重新分配内存**,使得**指向先前存储元素的所有引用、指针或迭代器失效**;
- 向容器中**移除元素**(例如移除堆顶、栈顶、队首队尾)后,则**原先==指向这些"位置" (而不是元素)==的引用仍有效**,但**其所指的已经不是上一个元素,而是新的堆顶/栈顶元素**。
> [!caution] **避免悬挂引用**:当引用指向容器中的元素时,确保在**==引用生命周期结束==**前,元素不会从容器中删除。
```cpp
vector<int> vec{1, 2, 3};
intk& ref = vec[0];
vec.push_back(4); // 可能导致内存重新分配, 使 ref 变为悬挂引用
```
```cpp
auto &p = min_pq.top();
min_pq.pop(); // 此后, 引用p仍然指向堆顶位置, 但是指向的是"新的堆顶元素", 而不是被移除的上一个堆顶元素.
```
<br><br><br>
# 引用的类型
C++中的引用可分为三种形式[^1](P166):
- **`non-const` 左值引用** (即常规引用)
- **`const` 左值引用**
- **右值引用** (since C++11)
不同引用形式旨在**支持对象的不同用法**(左值、常量、右值临时对象)。
> [!summary]
>
> - **==`non-const` 左值引用==** 只能绑定到==左值==(`lvalue`)对象,其不能与**字面值**或**右值**绑定。
> - "左值引用 `&` " 用于**引用具有持久存储位置的对象(左值 `lvalue `)**;
>
> >
>
> - **==`const` 左值引用==** 可绑定到**左值或右值**(例如 "**字面值**" 或 "**临时对象**","**某个表达式的计算结果**")。
>
> >
>
> - **==右值引用==** **只能绑定到==右值==(`rvalue`,包括 `xvalue` 和 `prvalue`)**
> - "右值引用 `&&` " 主要用于**实现 "移动语义" 和 "完美转发"**,可用于延长右值的生命周期,转移右值的资源。
<br><br>
## non-const 左值引用 `T&`
"**左值引用**"(lvalue reference)是对一个 "**左值 (`lvalue`) 表达式**" 的引用,也即 C++中的 **常规引用** 。
左值引用**不能到绑定右值( `xvalue` 和 `prvalue`)**。
左值引用本身仍然是 "**左值**",可被用于初始化另一个左值引用。
> [!NOTE] 左值
> 左值"通常"指的是 "**具有持久存储位置**的对象",即左值表示的对象在内存中**有确定的地址**(**可被取址 `&`**),它们可以出现在赋值语句的左侧。
<br>
## const 左值引用 `const T&`
const 左值引用即 "**对常量的引用**"(reference to const)——其目的是 **==不允许通过该引用来修改其所绑定的对象==(包括移动)**,通过 `const` 修饰符来禁止任何修改操作。
const 引用可以绑定到**左值或右值**(例如 "**字面值**" 或 "**临时对象**"),其常见用途是:
1. **延长临时对象的生命周期**:当一个临时对象被绑定到一个 `const` 引用上时,该临时对象的生命周期被延长(不会在表达式结束时被销毁),使其与 `const` 引用的生命周期一致;
2. 使**函数可以接受 "字面值" 和 "表达式" 作为参数**;
```cpp title: const_ref. cpp
// `non-const` 左值引用只能绑定到对象, 初始值只能是对象
// Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
// int& refval = 10;
// `const` 引用可以绑定到字面值和表达式。
const int& refval = 10;
```
<br><br>
## 右值引用 `T&&`
「**右值引用**」(rvalue reference)**专用于绑定到"==右值=="(`rvalue`,包括 `xvalue` 与 `prvalue`)**,其不能绑定到左值。
C++11 中引入"**右值引用**" 的目的在于**更有效地处理 "临时对象"**,其用途包括:
- (1)**实现 ==移动语义==**
- **将右值引用绑定到一个 `xvalue`**,对**即将被销毁的临时对象的资源**进行 "**==移动==**"。
- (2)**延长 ==右值对象== 的生命周期**(同 `const` 左值引用)
- **将右值引用绑定到一个 `prvalue` 或 `xvalue`**
> [!NOTE] 右值
>
> 右值不能被提取地址,通常是 "**临时对象**" 、"**不具有持久存储地址的表达式的结果**" ——包括**字面量**(字符串字面量除外)、**临时对象**、通过 `std::move()` 转换得到的对象(`xvalue`);
> [!important] **"右值引用" 本身是一个左值**,故可将其作为一个 "左值引用"的初始值,**通过一个"==左值引用=="来引用一个"==右值引用=="**:
```cpp title:r_ref_itself_is_a_lvalue.cpp
int&& r_ref = 5; // 5是一个右值(`prvalue`), 可将右值引用绑定到该字面量;
int& l_ref = r_ref; // 右值引用`r_ref`本身是一个左值, 因此可以被一个"左值引用"绑定.
int&& r_ref2 = std::move(r_ref); // 将std::move()将左值`r_ref`变成一个xvalue, 从而能被右值引用绑定
```
<br><br>
## "左值引用"与"右值引用"使用示例
```cpp title:lvalue_ref_and_rvalue_ref.cpp
int func() {
return 5;
}
// 左值引用与右值引用使用示例
void lvalue_ref_and_rvalue_ref() {
// 右值引用示例
int tmp = 99;
int&& r_ref1 = 5;
int&& r_ref2 = std::move(tmp); // `std::move`将变量名表达式tmp(lvalue)变为xvalue.
int&& r_ref3 = std::move(r_ref1); // `std::move`将变量名表达式r_ref1(lvalue)变为xvalue.
// int&& r_ref3 = tmp; // error: 变量名表达式tmp是lvalue. 不能将一个右值引用绑定到一个左值上.
// int&& r_ref4 = r_ref1; // error: 错误, 变量名表达式`r_ref1`是左值(尽管其是一个右值引用).
// 左值引用示例
vector<int> vi(100);
int i = 42;
int& l_ref1 = i; // 变量名表达式是 lvalue.
int& l_ref2 = vi[0]; // 下标运算符的结果是 lvalue, 只能用左值引用绑定.
int& l_ref3 = r_ref1; // 右值引用r_ref1其本身是个左值, 只能用左值引用绑定
// int l_ref4 = i * 25; // error: 算术表达式是prvalue, 不能绑定到左值引用
// 右值rvalue可被右值引用或const左值引用绑定:
// func()返回非引用类型, 该函数调用表达式为纯右值prvalue表达式, 可用右值引用或const左值引用绑定;
int&& r_ref = func(); // 将右值引用绑定到prvalue;
const int& cst_l_ref = func(); // 将const左值引用绑定到prvalue;
// 算术运算表达式的结果是prvalue, 可以用右值引用或左值引用来绑定
int&& r_ref_exp1 = vi[0] * func();
int&& r_ref_exp2 = i * 25;
const int& cst_l_ref_exp1 = vi[0] * func();
const int& cst_l_ref_exp2 = i * 25;
}
```
<br><br>
## 三种引用作为函数形参的区别
### 形参与实参的匹配
| 函数声明 | 形参 | 实参 |
| --------------------- | -------------------- | -------------------------------------- |
| `void foo(T);` | 按值传递 | - 对左值实参:**拷贝构造** <br>- 对右值实参:**移动构造** |
| `void foo(T&);` | **`non-const` 左值引用** | 只接受**左值**实参 |
| `void foo(const T&);` | **`const` 左值引用** | 可接受**左值**实参、**右值**实参; |
| `void foo(T&&);` | **右值引用** | 只接受**右值**实参 |
> [!NOTE] "拷贝构造" or "移动构造" 函数只会在 "按值传递" 的场景,涉及到 "创建副本" 时被调用
> [!caution] 右值引用只能绑定到右值,因此 **==传入实参只能是右值==**,而不能是右值引用。**==右值引用本身是左值==**。
> [!caution] "**const 左值引用**" `const T&` 允许绑定到一个 "**const 右值**"
>
> 如果实参是 **==const 右值==**(例如对一个 `const` 左值对象应用 `std::move()` 就会得到该结果,表达式是值类别是 `xvalue` ),
> 则上述函数重载版本中,**将调用 `void foo(const T&)`,而不是 `void foo(T&&)`**。
> 同理,**在类的成员重载版本中,调用的将是==拷贝构造函数== (例如 `Foo::Foo(const Foo&)`) ,而不是==移动构造函数== `Foo::Foo(Foo&&)`**。 ^1u415u
```cpp
/* 演示不同形式的引用作为"函数形参"时, 可接受的实参类型. */
struct MyClass {
MyClass() { cout << "Default Constructor" << endl; }
// 拷贝构造
MyClass(const MyClass& obj) { cout << "Copy Constructor" << endl; }
// 移动构造
MyClass(MyClass&& obj) noexcept { cout << "Move Constructor" << endl; }
};
void func1(MyClass obj) { cout << "Invoke func1(T)" << endl; }
void func2(MyClass& obj) { cout << "Invoke func2(T&)" << endl; }
void func3(const MyClass& obj) { cout << "Invoke func2(const T&)" << endl; }
void func4(MyClass&& obj) { cout << "Invoke func4(T&&)" << endl; }
int main() {
MyClass obj, obj2;
MyClass&& obj3 = MyClass {}; // 右值引用
// 1) 形参为 "按值传递" 时, 会调用构造函数:
func1(obj); // 实参为左值, 调用拷贝构造函数
// C++17强制要求对"临时对象"(prvaleu) 应用"拷贝省略"优化, 直接调用默认构造函数在目标位置构建.
// 在C++11下, 开启"-fno-elide-constructors"编译选项, 禁用编译器优化后, 将先调用默认构造函数, 再调用移动构造函数.
func1(MyClass{}); // 实参为右值(prvalue), 调用默认构造函数(由于拷贝省略优化)
func1(std::move(obj2)); // 实参为右值(xvalue), 调用移动构造函数.
cout << endl;
// 2) 形参为 "non-const 左值引用" 时:
func2(obj);
func2(obj3); // 可接受"右值引用", 因为右值引用本身是一个左值.
// 3) 形参为 "const左值引用", 实参可接受左右值
func3(obj); // 实参为左值
func3(MyClass{}); // 实参为右值(prvalue)
func3(std::move(obj2)); // 实参为右值(xvalue)
// 4) 形参为 "右值引用" 时, 实参只能接受右值
func4(MyClass{});
func4(std::move(obj2));
}
```
### **函数重载解析优先级**
- 当传入一个**左值实参**时,编译器会优先选择接受**左值引用形参**的重载版本;
- 当传入一个**右值实参**时,编译器会优先选择接受**右值引用形参**的重载版本;
- 如果没有可用的右值引用参数的重载,编译器会**尝试将右值绑定到接受 `const` 左值引用形参的版本上**( `const Type&`),因为 ` const ` 左值引用可以绑定到右值。
重载解析匹配示例一:
```cpp
void process(const string& s) { // 可接受左值实参、右值实参.
cout << "Invoke Process(const string&): " << s << '\n';
}
void process(string&& s) { // 只接受右值实参.
cout << "Invoke Process(string&&): " << s << '\n';
}
// 右值引用与const左值引用的重载解析
// 两个版本同时存在时, 对于右值实参, 优先调用右值引用形参的版本
int main() {
string lvalue = "This is a left value";
process(lvalue); // 调用process(const string&);
process("Right Value"); // 调用process(string&&);
process(std::move(lvalue)); // 将lvalue转换为右值, 调用process(string&&).
}
```
重载解析匹配示例二:
```cpp
// 未使用使用语义, 后两个语句将调用复制构造函数
namespace std {
template <typename T, ...>
class set {
public:
... insert (const T& x); // copy value of v
...
};
}
X x;
coll.insert(x); // inserts copy of x ...
coll.insert(x+x); // inserts copy of temporary rvalue ...
coll.insert(x); // inserts copy of x (although x is not used any longer)
// 使用移动语义后:
namespace std {
template <typename T, ...>
class set {
public:
... insert (const T& x); // for lvalues: copies the value
... insert (T&& x); // for rvalues: moves the value
...
};
}
X x;
coll.insert(x); // inserts copy of x (OK, x is still used) ...
coll.insert(x+x); // moves (or copies) contents of temporary rvalue ...
coll.insert(std::move(x)); // moves (or copies) contents of x into coll
```
<br><br><br>
# 引用折叠
> **引用折叠**(Reference Collapsing) [^2] [^3] [^4] [^5]
C++中允许通过 **模版** 或 **类型别名**(`typedef`、`using`)的类型操作**间接地形成一个类似 "引用的引用"类型**,
在这类场景下会触发 "**==引用折叠==**",所产生的实际类型只会是 C++所允许的**左值引用或右值引用**。
### 引用折叠规则
C++中的**两种引用折叠规则**如下:
| 规则 | 说明 |
| ------------------------------------- | ------------------------------------------------------- |
| `T& &` , `T& &&` , `T&& &` 都将折叠为 `T&` | 对 **"左值引用"的任意引用**、对 **"右值引用" 的左值引用**,都将折叠为 **==左值引用==** |
| `T&& &&` 将折叠为 `T&&` | 对"**右值引用的右值引用**" 将折叠为 **==右值引用==** |
### 引用折叠触发场景
在 **==任何可能构成 "引用的引用" 类型的上下文==** 中,都会触发"**引用折叠**",包括但不限于:
- **类型别名**上下文中
- **模版**上下文中
- **模版别名**上下文中
- **显式指定模版类型参数**的上下文中
- **转发引用**的上下文中
- **函数模版中的转发引用**;
- **`auto&&` 转发引用**;
### 类型别名上下文中的引用折叠
```cpp title:ref_collapsing_in_type_alias.cpp
// `typedef`声明类型别名上下文中的引用折叠
void type_alias_by_typedef() {
typedef int& lref;
typedef int&& rref;
int n = 1;
// 引用折叠
lref& r1 = n; // `int& &` -> `int&`
lref&& r2 = n; // `int& &&` -> `int&`
rref& r3 = n; // `int&& &` -> `int&`
rref&& r4 = 11; // `int&& &&` -> `int&&`
static_assert(std::is_same<decltype(r1), int&>::value);
static_assert(std::is_same<decltype(r2), int&>::value);
static_assert(std::is_same<decltype(r3), int&>::value);
static_assert(std::is_same<decltype(r4), int&&>::value);
}
// `using`声明类型别名上下文中的引用折叠
void type_alias_by_using() {
using lref = int&;
using rref = int&&;
int n = 2;
// 引用折叠
lref& r1 = n; // `int& &` -> `int&`
lref&& r2 = n; // `int& &&` -> `int&`
rref& r3 = n; // `int&& &` -> `int&`
rref&& r4 = 12; // `int&& &&` -> `int&&`
static_assert(std::is_same<decltype(r1), int&>::value);
static_assert(std::is_same<decltype(r2), int&>::value);
static_assert(std::is_same<decltype(r3), int&>::value);
static_assert(std::is_same<decltype(r4), int&&>::value);
}
```
### 模版别名上下文中的引用折叠
```cpp title:ref_collapsing_in_template_alias.cpp
// 模版别名
template <typename T> using lref_t = T&;
template <typename T> using rref_t = T&&;
// 模版别名上下文中的引用折叠
int main() {
int n = 5;
lref_t<int> a0 = n; // 'r0'是'int&'类型
rref_t<int> b0 = 35; // 'r3'是'int&&'类型
// 引用折叠:
lref_t<int&> r1 = n; // 'r1'是'int&'类型: `int& &` -> `int&`
lref_t<int&&> r2 = n; // 'r2'是'int&'类型: `int&& &` -> `int&`
rref_t<int&> r3 = n; // 'r3'是'int&'类型: `int& &&` -> `int&`
rref_t<int&&> r4 = 11; // 'r4'是'int&&'类型: `int&& &&` -> `int&&`
static_assert(std::is_same<decltype(r1), int&>::value);
static_assert(std::is_same<decltype(r2), int&>::value);
static_assert(std::is_same<decltype(r3), int&>::value);
static_assert(std::is_same<decltype(r4), int&&>::value);
}
```
### 显式指定模版类型参数的上下文中
```cpp title:ref_collapsing_in_tempalte_arg_assignment.cpp
template <typename T>
void FuncLvalueRef(T& param) {} // 左值引用
template <typename T>
void FuncForwardingRef(T&& param) {} // 转发引用/万能引用
int main() {
int i = 5;
int &lref = i;
int &&rref = 9;
// 对函数形参`T&`中的模版参数`T`显式指定为`int&`, 引用折叠: `int& &` -> `int&`, 常规左值引用, 只能接受左值
FuncLvalueRef<int&>(i);
FuncLvalueRef<int&>(lref);
FuncLvalueRef<int&>(rref);
// FuncLvalueRef<int&>(5); // error: 无法将rvalue绑定到左值引用
// FuncLvalueRef<int&>(std::move(i)); // error: 无法将xvalue绑定到左值引用
// 对函数形参`T&&`中的模版参数`T`显式指定为`int&&`, 引用折叠: `int&& &` -> `int&`, 常规左值引用, 只能接受左值
FuncLvalueRef<int&&>(i);
FuncLvalueRef<int&&>(lref);
FuncLvalueRef<int&&>(rref);
// FuncLvalueRef<int&>(5); // error: 无法将rvalue绑定到左值引用
// FuncLvalueRef<int&>(std::move(i)); // error: 无法将xvalue绑定到左值引用
// 不是"完美转发", 但触发引用折叠 `int& &&` -> `int&`, 常规左值引用, 只能接受左值
FuncForwardingRef<int&>(i);
FuncForwardingRef<int&>(lref);
FuncForwardingRef<int&>(rref);
// FuncForwardingRef<int&>(2); // error: 无法将rvalue绑定到左值引用
// FuncForwardingRef<int&>(std::move(i)); // error: 无法将xvalue绑定到左值引用
// 不是"完美转发", 但触发引用折叠 `int& &&` -> `int&&`, 常规右值引用, 只能接受右值
FuncForwardingRef<int&&>(5);
FuncForwardingRef<int&&>(std::move(i));
// FuncForwardingRef<int&&>(i); // error
// FuncForwardingRef<int&&>(lref); // error
// FuncForwardingRef<int&&>(rref); // error
}
```
### 转发引用的上下文中
```cpp
template <typename T>
void func(T&& arg) { // `T&&`是万能引用, 模版类型参数`T`本身可以是左值引用或右值引用.
// 如果`T`为左值引用, 根据引用折叠`X& &&`->"X&", 函数形参为左值引用类型.
// 如果`T`为右值引用, 根据引用折叠`X&& &&`->`X&&`, 函数形参表现为右值引用类型.
}
int main() {
int x = 10;
int& x_ref = x;
// 模版类型参数`T`推导为`int&`, 因此`T&&`为`int& &&`, 最终折叠为`int &`左值引用.
func(x_ref);
}
```
<br><br><br>
# 转发引用 | 万能引用
> **转发引用**(Forwarding Reference),也称 **"通用引用"**(Universal Reference), "**万能引用**"(非官方术语)[^6],。
转发引用是 **"==类型推导==" 机制下的一种特殊引用形式**,其能够**同时接收左值与右值**,并**根据 "实参" 的==值类别== 自动推导出对应的引用类型**(推导为**左值引用或右值引用**)。
## 转发引用的语法形式
以下两种**语法形式**属于 "**转发引用**" [^7]:
1. **==函数模版== 中形参类型声明为 `T&&`** (不带 cv-限定符,`T` 为模版参数),且类型 `T` 需要由"**模版参数推导**" 得出时(而非显式指定)[^8]。
2. **==`auto&&` 类型推导==中,除了初始值为花括号初始化列表时**。
两种情况的共性是都存在 "**类型推导**"。
> [!NOTE] 函数模版形参为模版参数包 `T&&...` 时,也属于情况(1)
<br>
## 转发引用的工作原理
对于两种转发引用形式 `auto&&` 与 `T&&`,最终产出的实际类型经历了两个步骤:"**类型推导**" 与 "**引用折叠**"。
- **==类型推导==**
- **编译器的"自动类型推导"** 本身只是**对其中占位符 `auto` 与 `T` 的推导**,<br>根据**实参为左值 or 右值**,对应 **==推导为 `X&` 和 `X`==**,其中 `X` 为实参的值类型。
- **==引用折叠==**
- 在类型推导后,若 **`auto&&`/`T&&` 构成了 `X& &&`** 即 "引用的引用",则触发**引用折叠规则**,最终产出类型**折叠为 `X&`**。
> [!important] 转发引用 `auto&&` 与 `T&&` 的推导结果
>
> - 若初始值是**左值**(lvalue),则 `auto` / `T` 被推导为 **==左值引用== `X&`**,进而 `auto&&` / `T&&` 构成 `X& &&`,由 **引用折叠** 得到 **==左值引用类型== `X&`**。
> - 若初始值是**右值**(xvalue 与 prvalue),则 `auto` / `T` 会被推导为 **==值类型== `X`**,进而 `auto&&` / `T&&` 构成 **==右值引用类型 `X&&`==** 。
>
> 参见 [^9]
演示说明示例:
```cpp
/* 演示转发引用的 "推导行为" */
template<typename T>
void func(T&& arg) {
// 检查对于 "T" 的类型推导:
if (std::is_lvalue_reference<T>::value) {
cout << "T is lvalue referecnes\n";
} else if (std::is_rvalue_reference<T>::value) {
cout << "T is rvalue reference\n";
} else {
cout << "T is neither\n";
}
// 明确对于 "arg" 的类型推导:
if (std::is_lvalue_reference<decltype(arg)>::value) {
cout << "arg is lvalue referecnes\n";
} else if (std::is_rvalue_reference<decltype(arg)>::value) {
cout << "arg is rvalue reference\n";
} else {
cout << "arg is neither\n";
}
cout << endl;
}
int main() {
int x = 10;
int&& r = 15;
func(x); // lvalue; => T推导为int&, T&&引用折叠为int&
func(20); // prvalue; => T推导为int, T&&即为int&&
func(std::move(x)); // xvalue => T推导为int, T&&即为int&&
func(r); // lvalue(右值引用本身是左值) => T推导为int&, T&&引用折叠为int&
}
```
<br>
## (1)转发引用形式一:函数模版中的转发引用
![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-引用相关.assets/IMG-cpp-引用相关-DD02D21CD2EF7AC1A61E74C6CAA4D942.png|675]]
#### 区分普通引用
> [!caution]
>
> 当且仅当 **(1)==在"函数模版"==中**并且 **(2) ==涉及到模版参数推导==** 时,函数形参 `T&&` 才表现为特殊的 "**转发引用**"! **如果 `T` 已经被明确指定,则 `T&&` 仅表示一个常规的右值引用形参**。例如:
>
> - 对于一个普通函数, `T` 是确定的,因此 `T&&` 是确定类型的右值引用形参,**只能接受"右值" 实参**。
> - 对于一个函数模版,如果模版参数 `T` 已被 `<>` 明确给定,则**不再是"转发引用",而是普通引用**,例如:
> - 给定 `<int>`,函数形参变为 `T&&` -> `int&&`,普通右值引用,只能接受右值;
> - 给定 `<int&>`,函数形参变为 `T&&` -> `int& &&`,根据引用折叠规则,折叠为 `int&`,常规左值引用,只接受左值;
> - 给定 `<int&&>`,函数形参变为 `T&&` -> `int&& &&`,根据引用折叠规则,折叠为 `int&&`,常规右值引用,只接受右值。
>
说明示例:
```cpp title:in_func_template.cpp
template <typename T>
void FuncLvalueRef(T& param) {} // 左值引用
template <typename T>
void FuncForwardingRef(T&& param) {} // 转发引用/万能引用
int main() {
int i = 5;
int &lref = i;
int &&rref = 9;
// 函数模版的形参为对模版参数的左值引用`T&`
FuncLvalueRef(i);
FuncLvalueRef(lref);
FuncLvalueRef(rref);
// FuncLvalueRef(2); // error: 无法将rvalue绑定到左值引用
// FuncLvalueRef(std::move(i)); // error: 无法将xvalue绑定到左值引用
// 函数模版的形参为对模版参数的转发引用/万能引用`T&&`
FuncForwardingRef(i);
FuncForwardingRef(lref);
FuncForwardingRef(rref);
FuncForwardingRef(2);
FuncForwardingRef(std::move(i));
// error: 明确指定了模版参数`T`的类型, 不再属于完美转发, 而变成常规的"右值引用"形参, 只能接受右值
FuncForwardingRef<int>(5);
FuncForwardingRef<int>(std::move(i));
// FuncForwardingRef<int>(i); // error
// FuncForwardingRef<int>(lref); // error
// FuncForwardingRef<int>(rref); // error
// 不是"完美转发", 但触发引用折叠 `int& &&` -> `int&`, 常规左值引用, 只能接受左值
FuncForwardingRef<int&>(i);
FuncForwardingRef<int&>(lref);
FuncForwardingRef<int&>(rref);
// FuncForwardingRef<int&>(2); // error: 无法将rvalue绑定到左值引用
// FuncForwardingRef<int&>(std::move(i)); // error: 无法将xvalue绑定到左值引用
// 不是"完美转发", 但触发引用折叠 `int& &&` -> `int&&`, 常规右值引用, 只能接受右值
FuncForwardingRef<int&&>(5);
FuncForwardingRef<int&&>(std::move(i));
// FuncForwardingRef<int&&>(i); // error
// FuncForwardingRef<int&&>(lref); // error
// FuncForwardingRef<int&&>(rref); // error
}
```
#### 注意事项
> [!danger] 函数模版中的 "**转发引用**" 几乎可以 "**精确匹配**" 任何类型的实参
>
> 应当避免在已定义 "**转发引用**" 形参函数版本的情况下,声明其他重载版本
例如,**当存在有一个 "==转发引用==" 版本的构造函数时**:
- (1)当实参为 `T&` 类型时,该**转发引用构造函数**(以模版参数为 `T&` 进行实例化)更能 "**精确匹配**",将会覆盖 "**拷贝构造函数**"。
- (2)在其派生类中,**派生类调用基类构造函数时**,将触发 "**==转发引用构造函数==**" 版本,受到后者劫持。
如下所示:
```cpp
class Person {
public:
template<typename T> //完美转发的构造函数
explicit Person(T&& n)
: name(std::forward<T>(n)) {}
};
class SpecialPerson : public Person {
public:
SpecialPerson(const SpecialPerson& rhs) // 拷贝构造函数, 调用基类的完美转发构造函数.
: Person(rhs)
{ ... }
SpecialPerson(SpecialPerson&& rhs) // 移动构造函数, 调用基类的完美转发构造函数
: Person(std::move(rhs))
{ ... }
};
Person p("Nancy");
auto cloneOfP(p); // p是`Person&`类型, 因此将触发Person的"完美转发构造函数", 而不是"拷贝构造函数"(形参为const Person&)
```
##### 不适用转发引用的场景
例如,在下述代码逻辑场景,不适用以 "**转发引用**" 为形参。
```cpp
template <typename T>
void func(T&& val) { // `T`可能是左值引用类型, 或者值类型本身.
T t = val; // 是拷贝构造, 还是绑定一个引用?
t = fcn(t); // 赋值只改变t, 还是同时改变val?
if (val == t) { // 若`T`是引用类型, 则一直为true.
...
}
}
```
<br><br>
## (2)转发引用形式二:`auto&&` 转发引用
```cpp
int main() {
int i = 1;
const int ci = 2;
int* const cptr_i = &i;
const int* const cptr_ci = &i;
int &lref = i;
int &&rref = 9;
const int& lref_ci = i;
const int&& rref_ci = 9;
// `auto&&`为"转发引用/万能引用", 完全保留被引用对象的"const"属性以及值类别.
// `auto&&` 会保留源实参的顶/底层const属性
auto&& b1 = ci; // `auto&&` 推断为`const int&`
auto&& b2 = cptr_i; // `auto&&` 推断为`int* const&`
auto&& b3 = cptr_ci; // `auto&&` 推断为`const int* const&`
auto&& b4 = lref; // `auto&&` 推断为`int&`
auto&& b5 = rref; // `auto&&` 推断为`int&&`
auto&& b6 = lref_ci; // `auto&&` 推断为`const int&`
auto&& b7 = rref_ci; // `auto&&` 推断为`const int&`
auto&& b8 = 54; // `auto&&` 推断为`int&&`. 虽然是prvalue, 但没有const限定
auto&& b9 = std::move(2); // `auto&&` 推断为`int&&`. 是xvalue, 但没有const限定
}
```
示例:在 lambda 表达式的**形参类型推导**中,使用 "**==转发引用==**"
```cpp
// 示例: 一个可对几乎任意函数进行计时的lambda表达式
auto timeFuncInvocation = [](auto&& func, auto&&... params) {
// TODO: start timer;
std::forward<decltype(func)>(func)( // 对func和params都进行完美转发
std::forward<decltype(params)>(params)...
);
// TODO: stop timer and record elapsed time;
};
```
<br><br>
# 移动语义和完美转发
基于**右值引用**的设计,C++引入了 "**移动语义**" 和 "**完美转发**" 这两个重要概念,能够显著提高程序的性能:
**==移动语义==**
通过右值引用,C++支持**将一个对象的资源(如动态内存)从源对象 "移动"(而非拷贝)到目标对象**,通常用在**源对象是即将销毁的临时对象**的情况,通过将**右值引用作为函数参数或返回类型来实现**。当编译器识别到一个对象作为右值引用传递时,将**优先触发移动语义**,从而避免不必要的复制。
**==完美转发==**
通过使用右值引用和 `std::forward<>` 函数,可使得函数模版**将参数以"完全相同的类型"转发给其他函数**,即让函数模版**保持传入的函数参数的左值或右值属性不变地**传递给另一个函数,从而使得模版编程和泛型编程更加高效、简洁。
<br>
# 完美转发
> 完美转发(Perfect Forwarding)
完美转发是用于 **"函数模版"** 中的一种技术,用于**将实参的==值类别==(左值或右值)以及 ==const 属性==** 完全保留地**传递给另一个函数**。
在一个 **==函数模版==** 中进行" **完美转发**" 基于两点完成:
1. 在**函数模版**中声明形参为 "**==转发引用==**",从而接受任意实参并**获取其完整属性**(`const` 属性、值类别)
2. 在函数模版的定义中**使用实例化的 ==`std::forward<T>()`== 进行参数传递**,从而保持参数的 **原始值类别** 不变。
> [!info] 标准库中基于 "**==完美转发==**" 实现的函数:
>
> 下列函数基于 "**==完美转发==**" 实现,其接受元素类型构造函数的参数,转发给相应的构造函数。
>
> - STL 容器中的 `.emplace_back()` 等**置入函数**;
> - 创建**智能指针**的 `make_shared<>()`、`make_unique<>()` 函数。
<br>
### 使用说明
```cpp title:perfect_forwarding.cpp
// 在函数模版中对实参进行"完美转发"
template<typename T>
void wrapper(T&& param) { // 1) 声明模版函数的形参为 "转发引用".
OtherFunc(std::forward<T>(param)); // 2) 使用`std::forward`进行转发, 保留实参的原始值类别.
}
// 更通用的形式, 接受任意数量形参:
template<typename... Args> // 模版参数包, 支持可变模版参数.
void wrapper(Args&&... params) {
OtherFunc(std::forward<Args>(params...)); // 转发给OtherFunc
}
```
1. **转发引用 `T&&`** 支持接收任意值类别的实参(左值 or 右值),并且**保留实参的 `const` 属性**。
2. `std::forward<T>(param)` 实现了将 **函数==形参== `param`** 以 **函数==实参==的原始值类别(左值或右值)** 传递给 `OtherFunc`。
#### `std::forward<T>(param)` 的转发过程说明 ⭐
完美转发场景下,对于模版函数 `wrapper(T&& param)`,以 `int` 型实参为例,根据 **实参的值类别**,推导结果如下:
| 传给 wrapper 的实参值类别 | T 的推导 | T&&(即`std::forward<T>()` 返回类型) | `std::forward<T>()` 表达式的值类别 |
| -------------------- | ------ | ------------------------------ | --------------------------- |
| 左值 | `int&` | `int&` (引用折叠) | 左值 |
| 右值(xvalue & prvalue) | `int` | `int&&` | 右值 |
**传递给 `std::forward<T>` 的模版参数** `T` 只有两种可能的推导结果:`X&` 或 `X`(这里 `X` 指代具体类型)。
<br>
### 使用示例
函数模版中的完美转发:
```cpp
#include <utility> // std::forward
void OtherFunc(int& x) { cout << "Lvalue: " << x << endl; }
void OtherFunc(int&& x) { cout << "Rvalue: " << x << endl; }
// 实现完美转发
template <typename T>
void wrapper(T&& arg) {
OtherFunc(std::forward<T>(arg)); // 实现完美转发, 将`arg`的类型和值完美转发给`OtherFunc`.
}
template <typename... Args>
void wrapper(Args&&... ars) {
OtherFunc(std::forward<Args>(args)...);
}
// 实现完美转发, 转发多个参数
template <typename F, typename T1, typename T2>
void flip(F f, T1&& t1, T2&& t2) {
f(std::forward<T1>(t1), std::forward<T2>(t2));
}
int main() {
int x = 10;
wrapper(x); // 调用 OtherFunc(int&)
wrapper(15); // 调用 OtherFunc(int&&)
wrapper(20); // 调用 OtherFunc(int&&)
}
```
泛型 lambda 中的完美转发:
![[02-开发笔记/01-cpp/函数相关/cpp-lambda 表达式#^u95vez]]
#### 常见使用场景: 使用 "**转发引用**"&**"完美转发"** 进行资源转移
> 参见 [^10]
在需要进行资源转移的函数定义为 "**模版函数**",应用 "**转换引用**" & "**完美转发**",
从而**根据实参值类型(左值 or 右值)==自适应地触发目标类型的拷贝 or 移动赋值运算符==**。
> [!NOTE] 由此,可以不必分别重载 `const T&` 与 `T&&` 两个版本的普通函数
使用示例一:函数体内**使用形参进行 "赋值" 的场景**,根据形参的值类型,**自适应地拷贝 or 移动构造**。
```cpp
class Widget {
public:
template <typename T>
void setName(T&& newName) {
name = std::forward<T>(newName); // 如果是左值, 触发string的拷贝构造函数; 否则, 触发移动构造函数.
}
// 使用成员函数模版&完美转发, 这样就不需要分别重载下列两个函数版本:
// void setName(const std::string& newName) {
// name = newName;
// }
// void setName(std::string&& newName) { // 接收右值引用
// name = std::move(newName);
// }
private:
std::string name;
...
};
```
使用示例二:**返回 "==转发引用形参==" 的场景**,根据形参的值类型,**自适应地拷贝 or 移动构造**。
```cpp
template <typename T>
Fraction reduceAndCopy(T&& frac) { // 按值返回, 这里要明确, T仅接收Fraction对象(左值or右值).
frac.reduce();
return std::forward<T>(frac); // 如果是左值, 触发Fraction的拷贝构造函数; 否则为右值, 触发移动构造函数.
}
```
使用示例三:函数体内**使用形参调用另一函数的场景**,根据形参类型,**触发另一函数的不同版本**。
```cpp
template <typename T>
void logAndAdd(T&& name) {
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
std::string petName("Darla");
logAndAdd(petName); // 实参为左值string, 函数体内形参推导为左值引用, `names.emplace()`执行拷贝
logAndAdd(std::string("Persephone")); // 实参为右值string, 函数体内形参推导为右值引用, `names.emplace()`移动右值而不是拷贝
logAndAdd("Patty Dog"); // 实参为const char*, 函数体内直接调用 `emplace(const char*)` 版本进行构造, 无拷贝无移动
```
<br>
### 完美转发失效的情况 🚨
有两种可能原因会导致**完美转发失败**:
1. **编译器不能推导出`wrapper`的一个或者多个形参类型**,这种情况下代码无法编译。
2. **编译器==错误地==推导了 `wrapper` 的一个或者多个形参类型**,进而可能导致两种情况之一:
- (1)**推导出的形参类型,与被调用函数==可接受的形参类型==不一致,导致编译失败**;
- (2)**推导出的形参类型,匹配上了==预期外的函数重载版本==**,导致未预料行为。
触发上述完美转发失败情况的常见场景[^11]:
- (1)实参为 "**==花括号列表==**" 时,模版参数 **`T&&` 无法推导得到具体的 `std::initializer_list<X>` 类型** ;
(可以理解为**涉及到两步推导**了,要求先推导得到 `std::initializer<>`,再推导得到其中的元素类型,这是编译器不支持的)
- (2)实参为 **0 或者 NULL** 时,完美转发推导的类型为**整型**,而绝不是**指针类型**,因此不能被转发给 "**接收指针**" 的函数;
- (3)实参为 "只有声明而无类外定义的 **`const` 整型静态数据成员**" 时,**可能导致 "链接错误"**。
- 由于无类外定义,因此编译器可能执行 "**常量传播**" 优化而**省略为其实际分配内存**,导致**以其 `MyClass::StaticConstObj` 作为模版参数时并不能被应用**( `T&&` 推导为引用类型,然而其并不具有内存,因此**无法传递引用** )
- (4)实参为 "**==重载函数名==**" 或 "**函数模版名**" 时,这两个 "**名称**" 不具有类型信息(前者因为存在重载,后者因为是模版),因此**无法执行类型推导**。
- => 解决办法是,需要显式指定 "**函数指针类型**" 或给出 "**模版实例**"。
- (5)实参为 "**==位域==**" 类型时。**位域类型无法被直接寻址,因此没有任何指针或引用能绑定到位域**。
- 对 "**位域**" 类型,只能 "**按值传递**" => 故解决办法是先用位域初始化一个副本变量,再以其为函数实参。
情况一示例:
```cpp
vector<vector<int>> vec;
vec.emplace_back({1, 2, 3, 4}); // error: emplace_back()内部实现为完美转发, 转发给构造函数. 然而编译器无法推导出实参`{1, 2, 3, 4}`为`std::initializer<int>`类型;
vec.push_back({1, 2, 3, 4}); // right: 发生隐式类型转换, 调用了vector<int>以"std::initializer_list<>"为形参的构造函数.
```
情况四示例:
```cpp
void process(int);
void process(int, int);
using FuncType = void (*)(int, int);
wrapper(static_cast<FuncType>(process)); // wrapper是完美转发的包装函数.
```
情况五示例:
```cpp
struct MyClass {
std::uint_32 IHL:4, DSCP:6, ECN:2;
} obj;
auto tmp = static_cast<std::uint16_t>(obj.DSCP);
wrapper(tmp); // wrapper是完美转发的包装函数.
```
<br><br>
## `std::forward<>` 函数模版
> 参见[^12] [^7] [^13] [^14]
`std::forward<T>(param)` 的表达式值类别 **只与==模版参数 `T`== 的类型有关**,与实参 `param` 是左值还是右值无关。
在完美转发场景下,`std::forward<T>()` 用于**函数模版**中,`T` 是 **"==万能引用==" 形式的函数形参**,
在传入模版函数的实参为左值、右值时, **`T` 分别被推导为 `X&` 与 `X`**,从而影响了 `forward` 的结果。
### `std::forward<>()` 的实现
实现说明如下[^5]:
其返回类型为 `T&&`,基于 **"==引用折叠=="** 机制得到实际返回类型。以 `int` 为例:
- 当 `T` 为 `int` 或 `int&&` 时,则 `T&&` => `int&` 或 `int&& &&` => `int&&`,返回**右值引用**, 表达式值类别为 "**右值**"。
- 当 `T` 为 `int&` 时,则 `T&&` => `int& &&` => `int&` ,返回**左值引用** => 表达式值类别为 "**左值**"。
其形参类型,标准库重载了两个版本,分别如下,以**明确==区分==接收左值、右值 `arg`**:
- `remove_reference<T>::type& arg`
- `remove_reference<T>::type&& arg`
其形参**不能像 `std::move()` 采用 `T&&` 万能引用**,而是 **必须通过 `remove_reference<T>` 后再结合 `&` 或 `&&` 进行区分**,旨在 **==防止==引用折叠**。
例如,若 `T` 被推导为 `int`,则 `T&&` 将得到 `forward(int&&)` ,**无法接收外层函数的形参**(形参只会是左值)!
---
`forward` 实现示例:
```cpp
template <typename T>
T&& myforward(remove_reference_t<T>& t) noexcept {
return static_cast<T&&>(t);
}
template <typename T>
T&& myforward(remove_reference_t<T>&& t) noexcept {
return static_cast<T&&>(t);
}
```
> [!NOTE] 标准库对 `std::forward()` 的实现:两个重载版本
> ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-引用相关.assets/IMG-cpp-引用相关-4F1AFF7CEA5F4CC476C6CA01D35A8B02.png|451]]
<br>
##### ❓ 为什么需要重载两个版本
标准库对 `std::forward` 实现了两个重载版本,分别用于**接收左值、右值**,差异仅在于 "**形参类型**" 不同。
如果只是用于 "**完美转发**" 场景,实际上**并不需要两个重载**,因为无论函数**实参** 是左值还是右值,**==形参只会是左值==**("右值引用" 本身是左值)。
所以,**只需要一个接收左值的版本即可**,而且不需要带 `remove_reference<>`。
> [!NOTE] "**完美转发**" 场景下,只会触发 `forward()` 的左值的版本
> ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-引用相关.assets/IMG-cpp-引用相关-ACF6B45B848C0D1A9999E469F6B8C0A1.png|824]]
重载两个版本,并且 **结合 `remove_reference()`** 从而**明确区分调用**,**使得 `std::forward(arg)` 也可以接收右值 `arg`**。
> [!question] ❓ 暂未想到什么场景会直接传一个右值给 `std::forward()`?
---
##### 避免嵌套模版调用导致的编译错误
> [!caution] 假设 `std::forward<>` 的形参声明为 `T&&`(如同 `std::move()`),则在 "**==嵌套模版调用==**" 时将导致编译错误
**原因在于**:
`std::forward` 是用于在 "**函数模版**" 中进行 "**==参数转发==**" (保留值类别)的,**而函数形参一定是==左值==**(具名),
因此 `std::forward<T>` 必须依赖于 "**==显式模版参数==`T`**" 来完成 "**值类别**" 的传递。
(正是这点不同于 `std::move()`,后者不需要模版参数将由自动推导)
**示例**:
假设 `std::forward` 形参声明为 `T&&`,
如下例,实参 `42` 为右值, 因此在**模版函数 `wrapper` 中: `T` 推断为 `int`**,而 **`T&&` 为 `int&&` 右值引用,其本身是个左值**。
而 `myforward<T>` 以 `int`进行实例化,得到函数签名 `int&& myforward(int&&)`, 其形参是**右值引用**,**只能接受右值**,故报编译错误。
因此,**正确实现是使用 `remove_reference<T>::type&` 保证形参始终是左值引用**,从而避免上述问题。
```cpp
template<typename T>
T&& myforward(T&& t) {
return static_cast<T&&>(t);
}
void process(int& x) { std::cout << "L-value reference\n"; }
void process(int&& x) { std::cout << "R-value reference\n"; }
template<typename T>
void wrapper(T&& arg) {
process(myforward<T>(arg));
}
int main() {
wrapper(42); // 传入右值实参时, 将导致编译错误, 无法通过编译;
}
```
- `std::move()` 用于**得到右值**,其返回类型为 `remove_reference<T>::type&&`,旨在通过 `remove_reference<>` 移除" **对 `T` 类型推导**" 带来的影响,**防止引用折叠**。
- 例如 `T` 若推导为 `int&`,则引用折叠后 `T&&` 将得到 `int&` 类型
<br><br><br>
# 移动语义
C++11 中引入了移动语义,由 "**移动构造函数**" 与 "**移动赋值操作符**" 来提供**对资源移动**的实现,允许对旧对象的资源进行移动而非复制,从而大幅地提高效率:
- **移动构造函数**:它接受一个右值引用参数,实现对**该右值引用所绑定对象的资源的"移动"**(而不非复制)。
- **移动赋值操作符**:与移动构造函数类似,但用于赋值操作。
> [!NOTE] "移动" 的资源
>
> 移动的 "资源" 主要针对 "**动态分配的内存**" ,例如 **STL 容器存储的元素**。
>
> - 如果一个类类型**包含需要==动态内存分配==的==成员变量或者指针==**,则为该类实现移动构造函数是非常推荐、甚至必要的做法,**在涉及到动态内存管理时,"移动" 而非 "拷贝" 资源能够提供更高的效率**。
> >
> - 对于**基本数据类型**,移动构造函数与拷贝构造函数的开销基本相同。
>
> ^3qfpzd
> [!info] STL 标准库对移动语义的支持
>
> 在 C++11 中,可以**用容器保存 "不可拷贝的类型"**,只要其能被**移动**即可。
>
> - STL 容器、`string` 和 `shared_ptr` 类**既支持移动也支持拷贝**。
> - string 中有定义移动构造函数,**string 的移动构造函数进行了指针的拷贝**,而不是为字符分配内存空间然后拷贝字符。
> - IO 类和 `unique_ptr` 类**可以移动但不能拷贝**。
> [!caution] **`const` 对象不支持移动**,原因在于 `std::move()` **将得到一个 `const` 右值**,触发 "**拷贝语义**" 而不是 "移动语义"。
>
> ![[02-开发笔记/01-cpp/类型相关/cpp-引用相关#^1u415u]]
>
>
> [!NOTE] 存在几种情况,C++11 的移动语义并无优势
>
> - **没有移动操作**:要移动的对象没有提供移动操作,所以移动的写法也会变成复制操作。
> - **移动不会更快**:要移动的对象**提供的移动操作并不比复制速度更快**。
> - **移动不可用**:进行移动的上下文要求移动操作不会抛出异常,但是移动操作本身没有被声明为`noexcept`
>
<br>
## 移动语义的使用
"移动语义" 体现在以 "**右值引用**" 类型为形参的函数,其可供 `non-const` "**右值**" 实参调用(xvalue & prvalue) :
- **移动构造函数**
- **移动赋值运算符**
- **其他以右值引用为形参的函数**,例如 `vector` 容器的 `push_back()` 重载版本之一。
```cpp
// 移动构造函数与移动赋值运算符
class X {
public:
X (const X& lvalue); // copy constructor
X (X&& rvalue) noexcept; // move constructor
X& operator= (const X& lvalue); // copy assignment operator
X& operator= (X&& rvalue) noexcept; // move assignment operator
...
};
// 其他函数
template <typename T>
void func(const T&); // 适用于拷贝语义场景,可接收左值, 右值, const右值
template <typename T>
void func(T&&); // 适用于移动语义场景, 只可接受non-const右值
```
> [!caution] "移动语义" 函数的形参形式
>
> - 对于**移动语义**函数,**形参应为 `X&&`**(精确匹配 **`non-const` 右值**),**而不能是 `const X&&`** !
> - 右值引用本身是一个 "左值",若 **声明为 `const` 意味着不可对其修改**,则**将源对象的资源移动后,无法将其置为无效状态**!(例如不能将其指针置空)
>
> - 对于**拷贝语义**函数,**形参应为 `const X&`**(**从而可接受左右值**),而非 `X&`(只能接收左值)。
>
>
> [!caution] 移动构造函数、移动赋值运算符应当显式标记为 `noexcept`
>
> 这是一种非常推荐的做法,尤其是在涉及到容器和算法中元素重排、复制和移动时。
>
> 标记为 `noexcept` 的主要原因在于 "**异常安全性**" 和 "**性能优化**"。
>
> - **==异常安全性==**:移动操作本质上应当是安全的,不抛出异常。因为移动操作通常**涉及简单的资源所有权转移,理论上不应该有失败的情况**。标记为 `noexcept` 显式表明该移动操作**保证不会抛出异常**,这有助于提高代码的健壮性。
> >
> - **==性能优化==**:**STL 容器(如 `std::vector`)在需要重新分配内存(扩容)、或在算法中需要重新排列元素**时,需要保证异常安全性:
> - 仅当元素的**移动构造函数和移动赋值运算符==被明确标记为== `noexcept`,==保证不会抛出异常==时,编译器和标准库实现才会优先于使用==移动语义==而不是拷贝语义**。
> - 否则,**如果移动操作可能抛出异常,则容器会使用==拷贝操作==来维持其异常安全性保证**。
>
> >
> - **标准库兼容性**:某些标准库模板(如 `std::move_if_noexcept` 、`std::is_nothrow_move_constructible_v<T>`)会检查操作是否被标记为 ` noexcept `,并**据此决定是移动对象还是拷贝对象**。
>
> ^yh06d4
> [!info] STL 容器的异常安全性保证
>
> vector 容器本身需要保证在调用 `puch_back()`、重新分配内存时**如果发生异常,vector 自身不会改变** [^15]。
>
> 因此 **vector 仅在元素类型的==移动构造函数==保证不会抛出异常(有 `noexcept` 声明)时才会调用该函数**。
> 否则,在重新分配内存过程中如果移动构造函数**在移动了部分而不是全部元素后抛出异常**,则会导致**旧空间中的移动源元素已经被改变**,而**新空间中未构造的元素可能尚不存在**。
>
> 而对于==**拷贝构造函数**==,其在新内存中构造元素时,旧元素保持不变。因此即使发生异常,vector **只需释放掉新分配(但还未构造成功)的内存并返回即可**,不会影响旧空间,**不影响原有的元素**。
> [!NOTE] `std::vectpr::push_back()` 检查元素的移动构造函数是否声明为 `noexcept` 的方式:
> - 其调用 `std::move()`的变体 `std::move_if_noexcept()`,后者**根据参数类型的移动构造函数是否为 `noexcept` 而视情况转换为右值或保持左值**。
> - `std::move_if_noexcept()` 具体是查阅 `std::is_nothrow_move_constructible` 这一 type trait 来进行检查。
<br><br>
## 移动语义使用示例
#### 当函数内需要将 "形参" 执行拷贝时的实现方案
当一个函数内 "**需要将==形参==作为==拷贝 or 移动的源对象==时**",若需根据 "**==实参类型==**" 而在函数体内**对应地执行 "拷贝语义"或 "移动语义"** 时,有三种可选方案:
- (1)定义一个 "**==按值传递==**" 的函数,内部**使用 `std::move(param)` 触发对形参的 "移动语义"**。
- (2)定义两个分别**接收==左值引用== `const T&` 与==右值引用==`T&&`** 的重载版本;
- (3)定义一个接收 "**==通用引用==**" 的 **==函数模版==**。
**若形参 `T` 类型的==移动成本低==**,则可考虑采用 **==按值传递==** 的方案,
相较于 "**重载两个版本**" 而言,改为 "**按值传递**" 只会多出一次额外的 "**==移动操作==**" 开销。
> [!summary] 三种方案的效率分析
>
> - 方案 1:相较于方案 2,**多出一次 "==移动==" 开销**("**一拷贝+一移动**" or **"两次移动"** 的开销)
> - 参数为 "**按值传递**",故根据**实参为左值 or 右值**,存在一次 "**==拷贝 or 移动构造==**" 开销。
> - 函数内 "**将形参转为右值**" 进行拷贝,故存在一次 "**==移动==**" 开销。
>
> - 方案 2:只存在 **"一次拷贝或移动"** 的总开销。
> - 两个重载函数形参均为 "引用",故**参数传递过程零开销**;
> - 对于左值重载,函数内将发生一次 "**==拷贝==构造或拷贝赋值**" 的开销;
> - 对于右值重载,函数内将发生一次 "**==移动==构造或移动赋值**" 的开销;
>
> - 方案 3:模版最终会实例化出两个版本,**同方案(2)的开销是完全相同的**。
>
```cpp
// (1) 按值传递的版本
// - 1) 发生一次拷贝构造或移动构造: 实参=>形参, 得到形参 str;
// - 2) 发生一次移动构造: 调用`.push_back(std::move(str))`时
// 故, 共两次构造函数的调用, 相较于上面两个重载版本, 多出了一次额外的"移动构造".
void addStr(std::string str) {
vec.push_back(std::move(str));
}
// (2) 定义两个分别接受const左值引用与右值引用的重载版本
void addStr(const std::string& str) {
vec.push_back(str); // 发生一次拷贝构造: 调用`.push_back(str)`时
}
void addStr(std::string&& str) {
vec.push_back(std::move(str)); // 发生一次移动构造: 调用`.push_back(std::move(str))`时
}
// (3) 定义一个接收 "通用引用" 的函数模版
// 最终会实例化出两个版本, 同方案(2)的开销是完全相同的.
template <typename T>
void addStr(T&& t) {
vec.push_back(std::forward<T>(t));
}
```
<br>
## `std::move()` 函数
> 位于 `<utility>` 头文件中
`std::move(x)` 会将传入参数 **转为 =="右值引用"==** 返回(语句的**表达式值类型**为 "**==亡值== `xvalue`**"),从而**可被右值引用绑定**,故可触发 "**移动**" 语义。
> [!faq] `std::move()` 的作用是为传入参数返回右值,从而 "**==触发==**" 移动语义。
>
> **实现 "move" 过程的是移动构造函数、移动赋值运算符**。
>
<br>
### `std::move()` 的实现
说明 [^13] [^16]:
- 以 `T&&` 转发引用为形参,接受任何类型实参,无论左右值。
- 以 `remove_reference<T>::type&&` 为返回类型和强制转换类型,保证无论 `T` 推导类型是否为引用,一定**返回右值引用**类型。
以 `T` 为 `string` 为例:
- 对于一个**左值**实参,则函数模版被实例化为 `string&& std::move(string& t);`
- 对于一个**右值**实参,则函数模版被实例化为 `string&& std::move(string&& t);`
```cpp title:move_imple.cpp
// `std::move` 实现示例
template <typename T>
typename remove_reference<T>::type&& move(T&& t) { // 转发引用/万能引用
return static_cast<typename remove_reference<T>::type&&>(t);
}
// C++14起, 可以语法更简洁地实现:
template <typename T>
decltype(auto) move(T&& t) {
return static_cast<remove_reference_t<T>&&>(t);
}
```
> [!NOTE] 标准库对 `std::move` 的实现
> ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-引用相关.assets/IMG-cpp-引用相关-30C1C40EDD9FF6BA96E7654C56E18BF0.png|544]]
<br>
### 使用示例
常见使用情景:
- string 对象作为函数实参: `func(std::move(str_obj))`;
- STL 中: `vec.push_back(std::move(str_obj))`;
示例一:
```cpp
void move_use() {
// std::move()执行到右值的无条件转换。就 move()函数本身而言,它没有 move 任何东西。单独使用无任何效果。
// 实现move过程的是类的“移动构造函数/移动赋值运算符”,move()的作用是让编译器识别到右值引用从而触发调用。
std::string str1("hello");
std::string&& str2 = move(str1); // 声明一个对string对象的右值引用, 并赋值;
std::string str11("lalalal");
std::string&& str22 = move(str11); // 声明一个对string对象的右值引用, 并赋值;
std::cout << "str1: " << str1 << '\t' << "str2:" << str2 <<std::endl;
std::cout << "str11: " << str11 << '\t' << "str22:" << str22 << std::endl;
// 触发移动构造函数
std::string str3(move(str1));
// 触发移动赋值运算符
std::string str33;
str33 = move(str11);
// 下两行输出中, 源对象str1和str11已经变成了空字符串, 因为执行了“移动构造函数/移动赋值运算符”,
// 字符串内容“移动”给了str3和str33对象。
std::cout << "str1: " << str1 << '\t' << "str3:" << str3 << std::endl;
std::cout << "str11: " << str11 << '\t' << "str33:" << str33 << std::endl;
}
```
示例二:
```cpp
/* 使用移动语义实现一个通用swap函数 */
template<typename T>
void swap(T& a, T& b) {
T tmp {std::move (a)};
a = std::move(b);
b = std::move(tmp);
}
```
<br><br>
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
# ♾️参考资料
# Footnotes
[^1]: 《C++ 程序设计语言》
[^2]: 《C++ 程序设计语言》 P170
[^3]: 《C++ Primer》P609
[^4]: [Reference declaration#Reference_collapsing - cppreference.com](https://en.cppreference.com/w/cpp/language/reference#Reference_collapsing)
[^5]: 《Effective Modern C++》Item28
[^6]: [Reference declaration#Forwarding_references - cppreference.com](https://en.cppreference.com/w/cpp/language/reference#Forwarding_references)
[^7]: 《Effective Modern C++》Item 24
[^8]: 《C++ Primer》P608
[^9]: 《Effective Modern C++》(Item 1、Item2、Item24、Item28)
[^10]: 《Effective Modern C++》Item25、Item26
[^11]: 《Effective Modern C++》Item30
[^12]: [std::forward - cppreference.com](https://en.cppreference.com/w/cpp/utility/forward)
[^13]: 《Effective Modern C++》Item23
[^14]: 《C++ Primer》P614
[^15]: 《C++ Primer》P474
[^16]: 《C++ Primer》P611