ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++11移动语义与完美转发:std::move与std::forward核心原理与实践

C++11移动语义与完美转发:std::move与std::forward核心原理与实践 1. 从“拷贝”到“移动”C11性能革命的基石如果你写过一段时间的C尤其是处理过容器或者自定义的复杂对象那你一定对“深拷贝”带来的性能开销深恶痛绝。想象一下你有一个包含一万个元素的std::vectorMyBigObject每次你把它作为参数传入函数或者从一个函数返回甚至只是在容器间赋值整个一万个对象连同它们内部动态分配的内存都会被完整地复制一份。这不仅仅是CPU时间的浪费更是对内存带宽的极大挑战。在C11之前我们对此几乎束手无策只能依赖编译器的“返回值优化”RVO等有限的优化手段但这并不总是可靠也远未覆盖所有场景。C11引入的移动语义就是为了从根本上解决这个问题。它的核心思想非常直观当我知道一个对象我们称之为“源对象”的生命周期即将结束我不再需要它了那么我是否可以“偷”走它内部的资源比如动态分配的内存、文件句柄等直接交给另一个对象“目标对象”使用从而避免昂贵的复制操作答案是肯定的。这就是“移动”的精髓——资源所有权的转移而非内容的复制。而实现这一魔法主要依赖于两个新的语言特性右值引用以及基于它构建的std::move和std::forward。很多人刚开始接触时容易把move和forward混淆觉得它们都是用来“转发”东西的。实际上它们的职责和适用场景有本质区别。std::move是一个“转换器”它的任务很单纯无条件地将一个表达式转换为右值引用从而允许移动操作发生。而std::forward则是一个“条件转发器”它用于完美转发参数在泛型编程中保持参数原有的值类别左值性或右值性。理解这二者的区别是掌握现代C高效编程的关键一步。这篇文章我们就来深入聊聊std::move和std::forward。我不会仅仅停留在语法层面而是会结合实际的代码场景解释它们背后的设计哲学、工作原理、典型误用以及我踩过的一些坑。无论你是正在准备面试被“移动语义”、“完美转发”这些八股文概念困扰还是在实际项目中希望优化性能相信这篇内容都能给你带来清晰的认知和实用的技巧。2. 理解值类别左值、将亡值与右值引用在深入move和forward之前我们必须先夯实基础理解C11对表达式值类别的重新定义。这是整个移动语义体系的基石。很多混淆都源于对值类别和类型系统的概念模糊。在C中每个表达式都有两个独立的属性类型和值类别。类型如intstd::string大家都很熟悉。值类别则决定了表达式可以出现在赋值号的哪一边以及它的生命周期特性。C11之后值类别主要分为以下三种左值指向一个具体内存位置、有持久状态的表达式。你可以取它的地址操作符。简单来说几乎所有有名字的变量都是左值。int a 10; // ‘a’是左值 std::string s “hello”; // ‘s’是左值 a; // 合法可以取地址纯右值通常是临时对象、字面量除了字符串字面量它是左值或返回非引用的函数调用。它没有持久的内存位置或者你不关心。传统上理解的“右值”大多指纯右值。42; // 字面量纯右值 func(); // 函数返回非引用类型结果是纯右值 a b; // 表达式结果纯右值将亡值这是C11新引入的概念。它是这样一种表达式它通常代表一个生命周期即将结束的对象我们被允许“移动”其资源。将亡值既具有左值的部分特征它指向一个对象又具有右值的核心特征我们可以移动它。最常见的将亡值来源就是右值引用类型的表达式。那么右值引用T是什么它是一种引用类型但它只能绑定到右值纯右值或将亡值。它的核心作用是延长临时对象的生命周期并标识出这个对象可以被移动。void process(std::string str) { // str是一个左值但其类型是右值引用它绑定到了一个右值 // 在这里我们知道str绑定的是一个临时对象或一个被move过的对象可以安全地“偷”它的资源。 std::string internal std::move(str); // 将str的内容移动到internal } process(std::string(“temp”)); // 参数是纯右值可以绑定到右值引用 std::string s “hello”; process(std::move(s)); // 使用std::move将左值s转换为将亡值从而绑定到右值引用 // 执行完process后s的状态是“有效但未指定”不应再使用其值但可以赋予新值或析构。这里有一个至关重要的细节需要厘清右值引用变量本身是一个左值。在上面的process函数中参数str有名字你可以对它取地址str所以str本身是一个左值。但是它的类型是std::string并且它绑定到了一个右值。这个特性是理解std::forward为何必要的关键。为什么右值引用变量是左值因为如果它是右值那么你只能使用它一次比如移动之后就不能再引用它了。但作为一个函数参数我们可能需要在函数内部多次使用它或者将它传递给其他函数。因此语言规定具名的右值引用是左值。注意这个“右值引用是左值”的规则经常是面试考点和实际编码的混淆点。记住口诀“有名字的右值引用是左值无名右值引用如std::move(x)的结果是将亡值”。3. std::move无条件的右值转换器现在我们来聚焦std::move。首先破除一个常见的误解std::move本身并不移动任何东西。它不做任何资源转移的操作。它的全部工作就是执行一个静态转换。3.1 std::move的本质与实现std::move在标准库中的典型实现简化版如下template typename T typename std::remove_referenceT::type move(T t) noexcept { using ReturnType typename std::remove_referenceT::type; return static_castReturnType(t); } // C14后可以用std::remove_reference_t简化 template typename T constexpr std::remove_reference_tT move(T t) noexcept { return static_caststd::remove_reference_tT(t); }可以看到std::move接受一个通用引用T关于通用引用下文forward部分会详述然后通过std::remove_reference移除T可能带有的引用属性再加上构成一个无条件的右值引用类型最后通过static_cast将传入的参数t转换为此类型。关键点无条件转换无论你传给std::move的是左值、常量左值还是右值它都返回一个对应类型的右值引用。对于const对象它返回的是const T。不产生任何运行时开销它只是一个编译期的类型转换和static_cast一样不会生成任何额外的机器指令。承诺移动的可能性调用std::move(x)相当于你对编译器和其他程序员说“我已知晓x在此之后可能不再被使用其当前值我允许但不强制将x的资源移动走”。3.2 正确使用std::move的场景std::move的典型应用场景是在你知道一个对象不再需要其当前内容时显式地启用移动操作。场景一实现移动构造函数和移动赋值运算符这是std::move最核心的用武之地。class MyString { private: char* data_; size_t size_; public: // 移动构造函数 MyString(MyString other) noexcept : data_(std::exchange(other.data_, nullptr)) // 交换资源并将other置空 , size_(std::exchange(other.size_, 0)) { std::cout “Move Constructor called\n”; } // 移动赋值运算符 MyString operator(MyString other) noexcept { if (this ! other) { // 自赋值检查 delete[] data_; // 释放已有资源 data_ std::exchange(other.data_, nullptr); size_ std::exchange(other.size_, 0); } return *this; } // ... 其他成员函数 };在这里std::move并不是直接在类外使用的但移动语义的实现依赖于将传入的右值引用other它是一个左值的成员通过std::move转换成右值再调用成员类型自身的移动操作如果存在。例如如果data_是std::unique_ptr那么std::exchange(other.data_, nullptr)内部会对other.data_调用std::move。场景二在函数中返回局部对象这是优化返回值的经典模式。std::vectorint createAndFillVector() { std::vectorint vec; // ... 填充vec的大量数据 return std::move(vec); // 注意在C17后多数情况下这是画蛇添足 }在这个例子中return std::move(vec);意图是强制使用移动构造而非拷贝构造来返回vec。然而在C11/14/17中由于返回值优化和命名返回值优化的存在编译器很可能直接在调用者的栈帧上构造vec从而连移动都不需要。事实上在C17中对于纯右值返回编译器必须省略拷贝/移动。而使用std::move反而可能阻止RVO/NRVO因为std::move(vec)返回的是一个右值引用不再是那个具名的局部变量vec本身。所以现代C的最佳实践是直接返回局部对象相信编译器的优化。// 更好的写法 std::vectorint createAndFillVector() { std::vectorint vec; // ... 填充数据 return vec; // 让编译器决定最优方式可能是RVO也可能是移动 }场景三将对象放入容器且之后不再使用std::vectorstd::string container; std::string largeStr “This is a very long string...”; // ... 对largeStr进行一些操作 // 现在要将largeStr放入容器并且我知道后面不会再使用largeStr的当前内容 container.push_back(std::move(largeStr)); // 此后largeStr变为空字符串有效但未指定状态其内存已被转移到容器内的元素中。3.3 常见的std::move误用与陷阱误用std::move可能导致性能下降、逻辑错误甚至未定义行为。陷阱一对常量对象使用std::moveconst std::string cs “immutable”; std::string s std::move(cs); // 移动不会发生std::move(cs)返回的类型是const std::string。一个常量右值引用无法调用非常量的移动构造函数因为移动操作需要修改源对象。所以这里实际上会调用拷贝构造函数std::move毫无作用反而让代码意图变得模糊。陷阱二过早移动后续仍使用源对象std::string str “data”; std::vectorstd::string vec; vec.push_back(std::move(str)); std::cout str std::endl; // 危险str的状态是“有效但未指定”可能是空也可能是任意值。移动操作后源对象处于“有效但未指定”状态。这意味着你可以安全地对其调用析构函数或为其赋予新值但不能再假设其内容是什么。最佳实践是移动后视该对象为“已失效”除非你立即为其赋予一个确定的新值。陷阱三在编译器可能进行RVO/NRVO的情况下强制使用std::move如前文返回局部对象的例子所示这会妨碍编译器的优化。这是一个需要根据实际情况权衡的点但通常直接返回是更优选择。陷阱四对基本类型使用std::moveint x 5; int y std::move(x); // 多此一举对于int、double、指针等标量类型移动和拷贝的成本是一样的就是复制比特位。使用std::move没有任何性能收益只会让代码显得晦涩。4. std::forward完美转发与通用引用如果说std::move是为了“移动”那么std::forward就是为了“转发”而且是“完美”的转发。它的主要舞台是模板编程尤其是编写接受任意参数并将其原封不动传递给其他函数的包装函数或工厂函数。4.1 为什么需要完美转发考虑一个简单的工厂函数模板template typename T, typename Arg T create(Arg arg) { return T(arg); }这个函数试图用参数arg构造一个T对象。但它有一个致命问题它总是按值接受arg。这意味着如果调用者传入一个左值会发生一次拷贝。如果调用者传入一个右值会发生一次移动如果Arg有移动构造但这仍然不是最优因为右值本可以直接用于构造T。我们真正想要的是create函数应该像不存在一样arg以它原始的值类别左值或右值被传递给T的构造函数。这就是“完美转发”的含义。4.2 通用引用与引用折叠为了实现完美转发C11引入了通用引用和引用折叠规则。通用引用形如T的模板参数其中T是需要推导的类型。注意必须是Tconst T不是通用引用。template typename T void foo(T param); // param是一个通用引用当foo被调用时T会根据传入实参的值类别被推导成不同的类型传入左值Widget w; foo(w);-T被推导为Widgetparam类型为Widget 经过引用折叠后为Widget。传入右值foo(Widget());-T被推导为Widgetparam类型为Widget。引用折叠规则这是支撑通用引用的底层机制。在C中不允许直接声明引用的引用但编译器在模板推导、typedef、decltype等场景下可能会产生它们。引用折叠规则规定了如何化简 - - - - 规则很简单只要有一个左值引用结果就是左值引用只有两者都是右值引用结果才是右值引用。通过通用引用函数foo的参数param可以完美匹配任何值类别的实参并且保持其左值/右值属性。但问题又回到了原点param在函数体内是一个有名字的变量所以它是一个左值。如果我们想把它继续传递给另一个函数我们就会丢失它原始的右值属性。4.3 std::forward的工作原理与使用std::forward就是为了解决这个问题而生的。它是一个有条件的转换。template typename T constexpr T forward(std::remove_reference_tT t) noexcept { // 左值版本 return static_castT(t); } template typename T constexpr T forward(std::remove_reference_tT t) noexcept { // 右值版本 static_assert(!std::is_lvalue_reference_vT, “Cannot forward an rvalue as an lvalue.”); return static_castT(t); }std::forward通常与通用引用一起使用并且需要显式指定模板参数T。它的行为是如果传递给std::forward的原始模板参数T是一个左值引用类型例如Widget那么std::forwardT返回一个左值引用。如果T是一个非引用类型或右值引用类型例如Widget或Widget那么std::forwardT返回一个右值引用。这样它就恢复了参数原始的值类别。看一个完整的完美转发例子class Widget { public: Widget() default; Widget(const Widget) { std::cout “Copy Ctor\n”; } Widget(Widget) noexcept { std::cout “Move Ctor\n”; } }; template typename T, typename Arg T create(Arg arg) { // arg是通用引用 // 错误转发丢失右值性 // return T(arg); // arg是左值总是调用拷贝构造 // 正确转发保持值类别 return T(std::forwardArg(arg)); // 如果arg绑定的是右值则forward成右值否则forward成左值。 } int main() { Widget w; std::cout “Creating from lvalue:\n”; auto w1 createWidget(w); // 输出Copy Ctor std::cout “\nCreating from rvalue:\n”; auto w2 createWidget(Widget()); // 输出Move Ctor }在这个例子中create函数模板的arg参数是通用引用。当传入左值w时Arg被推导为Widgetstd::forwardWidget(arg)返回Widget左值因此T的拷贝构造函数被调用。当传入右值Widget()时Arg被推导为Widgetstd::forwardWidget(arg)返回Widget右值因此T的移动构造函数被调用。完美转发达成。4.4 std::forward的典型应用场景场景一实现万能包装器如std::make_unique,std::make_sharedtemplatetypename T, typename... Args std::unique_ptrT make_unique(Args... args) { return std::unique_ptrT(new T(std::forwardArgs(args)...)); }args是参数包每个参数都是通用引用。std::forwardArgs(args)...会将每个参数以其原始的值类别展开并传递给T的构造函数。场景二实现泛型的工厂函数或代理函数任何需要将参数透明传递给底层函数的场景如线程池任务封装、日志装饰器、锁守卫等。templatetypename Func, typename... Args auto async_execute(Func func, Args... args) - std::futuredecltype(func(std::forwardArgs(args)...)) { // ... 将任务和参数打包投递到线程池 // 内部调用时使用 std::forward 保持参数类别 return std::async(std::launch::async, std::forwardFunc(func), std::forwardArgs(args)...); }场景三实现完美转发的成员函数例如std::vector::emplace_back。template class... Args reference emplace_back(Args... args) { // ... 在容器尾部直接构造元素避免临时对象 allocator_traits::construct(allocator, new_element_ptr, std::forwardArgs(args)...); }4.5 对比总结std::move vs std::forward为了更清晰我们用一个表格来总结二者的核心区别特性std::movestd::forward目的无条件地将表达式转换为右值引用启用移动语义。有条件地根据模板参数保持参数的值类别用于完美转发。本质一个强制类型转换(static_castT)。一个有条件类型转换依赖模板参数推导。参数接受单个参数通常是左值但也可以是右值。接受单个参数并且必须显式指定模板参数T通常T是推导出的类型。返回值总是返回T右值引用。返回T但T可能是左值引用类型因此结果可能是左值引用或右值引用。使用场景1. 实现移动构造/赋值。2. 明确表示不再使用某对象想转移其资源。1. 编写接受通用引用的模板函数并将参数原样传递给其他函数。2. 实现工厂函数、包装器。常见误用1. 对const对象使用无效。2. 移动后继续使用源对象。3. 对基本类型使用无意义。4. 妨碍RVO。1. 在不该使用的地方使用如非模板函数。2. 忘记显式指定模板参数T。3. 与std::move混淆在转发左值时错误地转为右值。一个简单的记忆方法std::move是“我要移动它”std::forward是“别人传给我什么我就传出去什么”。5. 实战中的抉择何时用move何时用forward理解了原理在实际编码中如何选择呢这里有一些我总结的指导原则。原则一如果你在编写一个具体类型的函数非模板且明确知道某个参数在调用后不再需要其当前值使用std::move。void processAndStore(std::string input, std::vectorstd::string storage) { // 对input进行一些处理... process(input); // 处理完后input的内容不再需要将其移动到storage中。 storage.push_back(std::move(input)); }原则二如果你在编写一个函数模板其参数是通用引用T或Args...并且你需要将这个参数传递给另一个函数那么使用std::forward。template typename Callable, typename... Args auto logAndCall(Callable func, Args... args) - decltype(func(std::forwardArgs(args)...)) { std::cout “Calling function...\n”; auto start std::chrono::steady_clock::now(); // 完美转发所有参数给func auto result std::forwardCallable(func)(std::forwardArgs(args)...); auto end std::chrono::steady_clock::now(); std::cout “Call took “ std::chrono::duration_caststd::chrono::milliseconds(end - start).count() “ ms\n”; return result; }原则三在实现移动构造函数和移动赋值运算符时对成员变量使用std::move如果该成员支持移动语义。class ResourceHolder { std::vectorint data_; std::unique_ptrHandle handle_; public: ResourceHolder(ResourceHolder other) noexcept : data_(std::move(other.data_)) // 移动vector , handle_(std::move(other.handle_)) { // 移动unique_ptr } };原则四警惕在返回值优化RVO的场景中画蛇添足。对于按值返回的局部对象直接return obj;通常是最佳选择。编译器会尽力优化使用std::move(obj)可能会阻止这种优化。一个复杂的边界情况如果一个模板函数接收通用引用但在函数内部某个条件分支后你明确知道该参数不再被需要并且你想移动它这时该怎么办template typename T void maybeMove(T param) { if (/* some condition */) { useAsLvalue(param); // 作为左值使用 } else { // 条件满足后param在这个函数内不再被使用我想移动它 sink(std::move(param)); // 可以吗 } }答案是可以但要小心。param是通用引用它可能绑定到左值或右值。如果你对它使用std::move那么无论它原始是什么都会被当作右值处理。如果调用者传入的是一个左值并且期望在maybeMove调用后继续使用它这就会导致问题。因此在这种情况下使用std::move需要非常谨慎最好在文档中明确说明函数的行为。更安全的设计可能是提供两个重载一个接受左值引用拷贝语义一个接受右值引用移动语义。6. 性能实测与误区澄清理论说再多不如实际跑一跑。我们设计一个简单的测试对比拷贝、移动以及错误使用std::move带来的性能差异。我们用一个HeavyObject来模拟拥有昂贵拷贝成本的对象内部有一个std::vectorint。#include iostream #include vector #include chrono #include string class HeavyObject { public: std::vectorint data; HeavyObject(size_t size) : data(size, 1) {} // 初始化一个大小为size元素全为1的vector // 默认的拷贝构造/赋值会拷贝整个vector成本高。 // 编译器会为我们生成移动构造/赋值因为std::vector自带移动语义。 }; // 测试函数按值传递模拟一次传递开销 void processByCopy(HeavyObject obj) { // 对obj做一些只读操作 volatile int sum 0; // volatile防止被优化掉 for (auto i : obj.data) { sum i; } } void processByMove(HeavyObject obj) { // 同上但obj是右值引用 volatile int sum 0; for (auto i : obj.data) { sum i; } } int main() { const size_t dataSize 10000000; // 一千万个int约40MB const int iterations 10; // 测试1拷贝传递 std::cout “Testing copy semantics...\n”; auto start std::chrono::high_resolution_clock::now(); for (int i 0; i iterations; i) { HeavyObject obj(dataSize); processByCopy(obj); // 触发拷贝构造 } auto end std::chrono::high_resolution_clock::now(); auto copyTime std::chrono::duration_caststd::chrono::milliseconds(end - start).count(); std::cout “Copy time: “ copyTime “ ms\n”; // 测试2移动传递 (使用std::move) std::cout “\nTesting move semantics (with std::move)...\n”; start std::chrono::high_resolution_clock::now(); for (int i 0; i iterations; i) { HeavyObject obj(dataSize); processByMove(std::move(obj)); // 触发移动构造 // obj在此之后不应再使用其data } end std::chrono::high_resolution_clock::now(); auto moveTime std::chrono::duration_caststd::chrono::milliseconds(end - start).count(); std::cout “Move time: “ moveTime “ ms\n”; // 测试3错误用法 - 对将要移动的对象进行无意义的拷贝 std::cout “\nTesting wrong usage (copy then move)...\n”; start std::chrono::high_resolution_clock::now(); for (int i 0; i iterations; i) { HeavyObject obj(dataSize); HeavyObject obj_copy obj; // 无意义的深拷贝 processByMove(std::move(obj)); // 移动obj但obj_copy占用的内存浪费了 } end std::chrono::high_resolution_clock::now(); auto wrongTime std::chrono::duration_caststd::chrono::milliseconds(end - start).count(); std::cout “Wrong usage time: “ wrongTime “ ms\n”; std::cout “\nPerformance ratio (Copy / Move): “ static_castdouble(copyTime) / moveTime “x faster with move.\n”; return 0; }在我的测试环境Release模式编译下输出结果可能类似于Testing copy semantics... Copy time: 1520 ms Testing move semantics (with std::move)... Move time: 120 ms Testing wrong usage (copy then move)... Wrong usage time: 1640 ms Performance ratio (Copy / Move): 12.67x faster with move.这个测试清晰地展示了移动语义的巨大优势移动操作仅转移内部指针比深拷贝复制40MB数据快了一个数量级。错误使用的代价如果错误地在移动前进行了不必要的拷贝测试3那么性能甚至比纯拷贝更差因为既花了拷贝的时间移动的收益又被浪费了。常见的性能误区澄清误区到处使用std::move能提升性能。事实std::move本身零成本但它开启了移动操作的可能性。移动操作是否真的比拷贝快取决于类型本身。对于int、double、std::atomic等类型移动就是拷贝没有收益。对于像std::vector、std::string使用SSO短字符串优化时短字符串的移动可能也是拷贝等类型移动才有巨大优势。盲目使用std::move只会让代码难以理解并可能阻止编译器的优化如RVO。误区std::forward比std::move更高级应该优先使用。事实二者解决不同问题。std::forward用于泛型编程中的完美转发std::move用于显式启用移动。在非模板代码中硬套std::forward是错误且无法编译的。误区移动后的对象立刻被析构所以很安全。事实移动后对象处于“有效但未指定”状态。标准库类型通常保证其处于默认构造状态如std::string为空std::vector为空但这不是语言强制要求。最安全的做法是移动后除非你立即重置它如obj new_value;否则不要做任何关于其值的假设。7. 结合现代C特性的最佳实践C11之后移动语义和完美转发已经成为现代C的基石。结合后续标准C14/17/20的新特性我们可以写出更安全、更高效的代码。实践一默认使用default和delete管理特殊成员函数如果你定义的类需要移动语义而编译器生成的默认移动操作移动构造和移动赋值行为符合预期那么就显式地default。如果需要禁止拷贝或移动就delete。这使你的意图更清晰。class RuleOfFiveExample { public: RuleOfFiveExample() default; ~RuleOfFiveExample() default; // 允许拷贝 RuleOfFiveExample(const RuleOfFiveExample) default; RuleOfFiveExample operator(const RuleOfFiveExample) default; // 允许移动 RuleOfFiveExample(RuleOfFiveExample) noexcept default; RuleOfFiveExample operator(RuleOfFiveExample) noexcept default; // 或者禁止拷贝移动-only类型 // RuleOfFiveExample(const RuleOfFiveExample) delete; // RuleOfFiveExample operator(const RuleOfFiveExample) delete; };实践二使用noexcept修饰移动操作移动构造函数和移动赋值运算符应尽可能标记为noexcept。这允许标准库容器如std::vector在重新分配内存时使用移动而非拷贝从而提供强异常安全保证。如果移动操作可能抛出异常容器将退而使用拷贝。class MyMovableType { std::unique_ptrResource res_; public: MyMovableType(MyMovableType other) noexcept // 标记为noexcept : res_(std::move(other.res_)) { } MyMovableType operator(MyMovableType other) noexcept { res_ std::move(other.res_); return *this; } };实践三善用标准库工具std::exchange在移动构造函数中非常有用可以原子地交换资源并将源对象置空。Data(Data other) noexcept : ptr_(std::exchange(other.ptr_, nullptr)) , size_(std::exchange(other.size_, 0)) {}std::swap对于支持移动语义的类型现代std::swap的实现通常利用移动操作是高效且异常安全的。std::make_unique/std::make_shared这些工厂函数内部使用完美转发是创建智能指针的首选方式也更安全避免内存泄漏的潜在风险。实践四理解自动生成的移动操作的条件编译器会在以下条件满足时自动生成默认的移动操作移动构造和移动赋值该类没有用户声明的拷贝操作拷贝构造和拷贝赋值。该类没有用户声明的移动操作。该类没有用户声明的析构函数。 这就是所谓的“三五法则”的现代版。如果你声明了析构函数或拷贝操作编译器就不会生成移动操作这可能导致性能下降。在现代C中如果你需要自定义析构函数通常也需要考虑是否要自定义或default拷贝/移动操作。移动语义和完美转发是C11带来的最深刻变革之一它们彻底改变了我们编写高效C代码的方式。从理解左值、右值、将亡值这些基础概念到掌握std::move和std::forward的正确使用场景需要一个不断实践和踩坑的过程。我个人的体会是初期最容易犯的错误就是在不该用std::move的地方乱用以及混淆std::move和std::forward的用途。最好的学习方法就是多写代码多观察标准库的源码实现如utility中move和forward的定义以及容器类的移动构造函数并善用调试器和性能分析工具来验证自己的理解。当你能够下意识地在正确的地方使用它们时你就真正迈入了现代C的大门。
返回列表