Item29:认识移动操作的缺点
移动语义是 C++11 最核心的特性之一:它能在满足特定条件时,用低开销的移动操作替代高开销的复制操作,甚至能让 C++98 的代码重新编译后直接获得性能提升。但也正因如此,移动语义的效果和适用范围被普遍夸大,本条款的目的是让开发者理性看待移动语义,避免过度乐观。 移动语义无优势 / 失效的场景移动语义无法带来性能提升、甚至直接退化为复制操作的四类典型场景: 类型本身不支持移动操作:对于 C++98 遗留的未适配类型、不符合编译器默认生成移动操作条件的自定义类型(如声明了复制操作、移动操作或析构函数,或成员 / 基类禁用了移动),移动写法会直接退化为复制,无法带来任何性能提升。 移动操作并不比复制更快:并非所有支持移动的类型都有低开销的实现。比如 std::array,数据直接存储在对象本身而非堆内存,移动需要线性时间逐个处理元素,开销与复制处于同一量级;再比如开启了小字符串优化(SSO)的短 std::string,数据存储在对象内部缓冲区,移动开销与复制几乎无差异。 移动操作被上下文强制禁用:标准库部分容器操作要求强异常安全保证,若移动操作未声明noex...
Item30:熟悉完美转发失败的情况
完美转发 转发:把一个函数的形参(限定为引用类型,避免拷贝原始对象、不强迫调用者传指针)传递给另一个函数,目标是让被转发的函数拿到和原始调用完全相同的对象。 完美:不仅转发对象本身,还转发对象的核心特征 —— 类型、是左值 / 右值、是否 const/volatile。 实现方式:通过 “通用引用”+std::forward实现,通常写成可变模板(支持任意数量参数),模板结构如下: 1234template<typename... Ts>void fwd(Ts&&... params) { f(std::forward<Ts>(params)...); // 转发给目标函数f} 失败判定:直接调用f(表达式)和通过fwd(表达式)调用的行为不一致(编译失败 / 执行逻辑不同),就说明完美转发失败了。 完美转发失败的 5 类场景1. 花括号初始化器(如{1,2,3}) 失败示例: 123void f(const std::vector<int>& v);f(...
Item28:理解引用折叠
引用折叠是 C++11 引入的底层编译期规则,是通用引用(转发引用)、std::forward能正常工作的核心 ——C++ 语法明确禁止显式声明 “引用的引用”(比如int& &),但编译器在特定场景下会隐式生成引用的引用,此时靠 “引用折叠规则” 将其化简为单个引用,这也是通用引用能同时绑定左值 / 右值的根本原因。 通用引用的类型推导编码规则对于通用引用模板: 12template<typename T>void func(T&& param); // param是通用引用 模板参数T会根据传入实参的 “值类别”(左值 / 右值)做编码推导: 传入左值 → T推导为左值引用(比如传入Widget左值,T= Widget&); 传入右值 → T推导为非引用类型(比如传入Widget右值,T= Widget)。 这种 “不对称编码” 是理解引用折叠的关键,比如: 12345Widget w; // 左值Widget widgetFactory(); // 返回右值的函数func(w)...
Item32:使用初始化捕获来移动对象到闭包中
C++11 lambda 的捕获短板我们知道 C++11 的 lambda 只有按值捕获和按引用捕获两种方式,但这两种方式都有局限,满足不了一些特殊场景: 对于只能移动、不能拷贝的对象,比如std::unique_ptr、std::future,C++11 根本没法把它们放进闭包; 对于拷贝成本很高的对象(比如大容器std::vector),我们只想移动进闭包节约开销,C++11 也做不到,只能被迫拷贝。 这是 C++11 lambda 设计上的一个明显缺陷,而 C++14 直接补上了这个短板。 C++14 的完美解决方案:初始化捕获(init capture)C++14 引入了初始化捕获,也叫通用 lambda 捕获,它的灵活性极强,移动捕获只是它的核心用法之一,甚至能直接在捕获里创建对象,完全覆盖了 C++11 捕获的功能,还能做更多事。 1. 初始化捕获的语法逻辑1[闭包内的数据成员名 = 初始化表达式] 等号左侧:是 lambda 闭包类里自定义的数据成员名称; 等号右侧:是初始化这个成员的表达式,可以是移动、函数调用、临时对象等。 两者作用域不同:左侧属于闭包...
Item34:考虑使用 lambda 而非 bind
lambda 全面优于 std::bind(C++14 中无例外)lambda 相比std::bind更易读、表达力更强、性能更高;仅在 C++11 的两个特殊场景下,std::bind有少量合理用途,而 C++14 中 lambda 的增强特性让std::bind完全失去使用价值。 可读性对比:lambda 直观,std::bind 晦涩且易踩坑以setAlarm(设置闹钟)为例,对比两者的实现差异: lambda 版本(清晰易懂)12345auto setSoundL = [](Sound s) { using namespace std::chrono; using namespace std::literals; // C++14时间字面量 setAlarm(steady_clock::now() + 1h, s, 30s); // 直接调用,参数清晰}; 直接调用目标函数setAlarm,参数传递、求值时机一目了然; 即使是 lambda 新手,也能快速理解 “lambda 的形参s传给setAlarm第二个参数”; C++14...
Item36:如果有异步的必要请指定std::launch::async
std::async 默认启动策略的本质与问题std::async的默认启动策略并非单一的异步执行(std::launch::async),而是std::launch::async | std::launch::deferred(异步 + 延迟执行的组合)。其底层逻辑是:系统自主决定任务是异步在新线程执行,还是延迟到调用future的get()/wait()时同步执行。 这一设计的初衷是让标准库灵活管理线程资源(避免资源超额 / 线程耗尽),但核心问题是:默认策略完全失去了对任务执行方式的可控性,引发一系列不确定性。 默认策略引发的执行不确定性坑 1:执行方式不可控 无法预测任务是否与调用std::async的线程并发执行; 无法预测任务是否在调用get()/wait()的线程(而非新线程)上执行。 坑 2:线程本地存储(TLS)访问混乱若任务读写thread_local变量,无法确定访问的是 “新线程的 TLS” 还是 “调用get()/wait()线程的 TLS”。 坑 3:任务可能永不执行若程序所有路径都未调用future的get(...
Item33:对于auto&&形参使用decltype以及forward它们
泛型 lambda 的本质与初始问题C++14 的泛型 lambda 允许在形参中使用auto,它的底层实现是:lambda 对应的闭包类中,operator()会被编译成一个模板函数。比如: 1auto f = [](auto x) { return func(normalize(x)); }; 等价于闭包类里的模板成员函数: 1234567class CompilerGeneratedClass {public: template<typename T> auto operator()(T x) const { return func(normalize(x)); }}; 这里的问题是:即使你给 lambda 传入的是右值(比如临时对象),形参x本身是左值,导致normalize永远接收到左值,无法区分实参的原始值类别(左值 / 右值),失去了 “转发” 的意义。 完美转发的初步尝试与障碍要实现 “完美转发”(保持实参的左值 / 右值属性),常规思路...
Item35:优先考虑基于任务的编程而非基于线程的编程
两种异步执行方式的直观对比基于线程(thread-based):直接创建 std::thread12int doAsyncWork();std::thread t(doAsyncWork); // 直接创建线程执行函数 无直接获取返回值的方式; 若函数抛出异常,程序会直接调用std::terminate终止。 基于任务(task-based):使用 std::async1auto fut = std::async(doAsyncWork); // 提交任务,返回future对象 代码更简洁; future的get()函数可获取返回值,还能捕获函数抛出的异常(避免程序终止); 线程管理的责任交给标准库,而非开发者。 理解线程的三层含义 线程类型 定义 硬件线程 CPU 核心提供的真实执行单元,是计算的物理载体。 软件线程(OS 线程) 操作系统管理的线程,运行在硬件线程上,数量可多于硬件线程(阻塞时 OS 调度其他线程)。 std::thread C++ 中 “软件线程” 的句柄,可能为空(默认构造、移动、join、detach 后无对应软件线程)。 ...
Item37:使thread在所有路径最后都不可结合
std::thread 的可结合 / 不可结合状态本质std::thread 对象仅有两种状态,其中可结合状态是风险点: 可结合(joinable):对应 “正在运行 / 待调度 / 已结束但未执行 join/detach” 的底层线程,此状态下析构 std::thread 会直接导致程序终止; 不可结合(unjoinable):安全状态,包括默认构造、已移动、已 join、已 detach 的 std::thread。 可结合线程析构的致命问题(默认处理方式的弊端)标准委员会拒绝为 std::thread 析构提供默认处理(隐式 join/detach),因为两种方式均存在严重问题: 弊端 1:隐式 join(等待线程完成)会引发反直觉的性能异常(比如条件不满足时,仍等待耗时的过滤逻辑执行完毕),调试难度大; 弊端 2:隐式 detach(分离线程)会导致未定义行为:若线程捕获了函数局部变量,函数返回后局部变量销毁,线程仍访问已释放的栈内存,造成内存篡改,调试成本极高。 解决问题:RAII 封装 std::thread(覆盖所有执行路径)要确保...
Item39:对于一次性事件通信考虑使用void的futures
一次性事件通信 方向:检测任务 → 反应任务(单方向); 内容:仅传递 “事件已发生” 的信号,无需传递具体数据; 次数:仅一次(事件触发后无需重复通知); 要求:反应任务需真正阻塞(不占用 CPU)、无通知丢失、无虚假唤醒。 方案 1:条件变量(condvar)—— 设计别扭的 “标配”1. 多余的互斥锁(代码异味)条件变量必须配合互斥锁使用(C++11 API 强制要求),但一次性事件通信中,检测 / 反应任务往往无共享数据需要保护(比如检测任务初始化完数据结构后不再访问,反应任务仅在事件后访问)—— 互斥锁成了 “无意义的强制依赖”。 2. 通知丢失风险若检测任务在反应任务调用 cv.wait() 前执行 cv.notify_one(),反应任务会永久挂起(因为 wait 时通知已丢失)。 3. 虚假唤醒问题条件变量存在 “虚假唤醒”(即使未被通知,wait 也可能返回),因此必须配合 “条件判断” 使用: 1234567std::condition_variable cv;std::mutex m;bool flag = false;// 反应任务std::u...
