%% # 纲要 > 主干纲要、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] 函数模版中的 "**转发引用**" 几乎可以 "**精确匹配**" 任何类型的实参 > > 应当避免在已定义 "**转发引用**" 形参函数版本的情况下,声明其他重载版本![^7](Item 26) 例如,**当存在有一个 "==转发引用==" 版本的构造函数时**: - (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