Item21:优先使用make_unique和make_shared而非直接使用new
std::make_shared是 C++11 标准的,而std::make_unique直到 C++14 才加入; 如果用 C++11,可手写简易版make_unique(仅支持基础场景,不支持数组 / 自定义析构),但注意不要放到std命名空间,避免和 C++14 标准库冲突; 三个 make 函数:make_unique、make_shared、allocate_shared(allocate_shared和make_shared行为一致,只是支持自定义内存分配器),它们的共性是 “接收任意参数→完美转发给对象构造函数→动态分配对象→返回对应智能指针”。 优先用 make 函数避免类型重复直接用new创建智能指针时,类型会写两次,而 make 函数只需写一次,符合 “避免重复代码” 的软件工程原则: 1234567// 重复写Widget:不够简洁std::unique_ptr<Widget> upw2(new Widget);std::shared_ptr<Widget> spw2(new Widget);// 只写一次Widget:...
Item22:使用Pimpl习惯用法时,将特殊成员函数的定义放到实现文件中
Pimpl 的价值Pimpl 的目的是减少编译依赖、加快编译速度: 传统写法中,Widget的头文件依赖std::string、std::vector、Gadget等头文件,一旦这些依赖变更,所有包含Widget.h的代码都要重新编译; Pimpl 把这些依赖移到.cpp文件中,头文件仅前向声明Impl结构体,用指针指向它,彻底切断 “头文件 - 实现依赖” 的关联。 C++98 用原始指针实现 Pimpl,现代 C++ 优先用std::unique_ptr(因为 pImpl 是 “独占所有权”),但直接用unique_ptr会踩坑。 用std::unique_ptr做 Pimpl 会编译报错1. 报错现象头文件仅前向声明Impl,用std::unique_ptr<Impl> pImpl,且未手动声明析构函数时,用户创建Widget对象会编译报错(提示 “sizeof/delete 应用到不完整类型”): 123456789101112// widget.h(有问题的写法)class Widget {public: Widget() : ...
Item23:理解move和forward
两者的本质是编译期类型转换,运行时无任何操作 std::move不移动任何数据,std::forward不转发任何数据; 它们都是函数模板,仅在编译期对参数做值类别转换(左值→右值,或条件性转换),运行时不生成任何可执行代码(一字节都没有); 两者的核心作用都是改变参数的值类别(左值 / 右值),而非操作数据本身。 std::move:无条件将参数转为右值(但不保证能移动)1. 本质拆解接收一个通用引用参数,通过static_cast无条件转换为非引用类型的右值引用(用std::remove_reference确保不会因为 T 是左值引用而变成左值),最终返回一个右值。 简单说:std::move(x)的唯一作用是把x标记为右值,告诉编译器 “这个对象可以被移动”—— 但这只是 “提议”,不是 “强制”。 2. const 对象用 std::move 会 “伪移动”(实际是拷贝)123456789// 错误示范:给想移动的对象加了constclass Annotation {public: explicit Annotation(const std::...
Item24:区分万能引用和右值引用
T&&有两种身份,并非只有右值引用 我们默认T&&是右值引用,但事实是,T&&在源码中可能有两种完全不同的含义; 第一种:真正的右值引用—— 只能绑定到右值(临时对象、std::move后的对象),核心作用是识别可移动的对象; 第二种:通用引用(也叫转发引用)—— 本质是 “二重性引用”,源码上写的是T&&,但可以表现为左值引用或右值引用,能绑定几乎任何东西(左值 / 右值、const/non-const、volatile 等); 通用引用是一种 “抽象”(而非底层真相),底层是 “引用折叠”,但这种抽象足够实用,能帮我们快速区分两种引用。 通用引用的判断:必须同时满足两个条件1:存在类型推导 要么是函数模板形参,T(或模板参数)需要在函数调用时自动推导(排除调用者显式指定类型的边缘情况); 要么是auto&& 声明的对象,auto的类型需要在初始化时自动推导; 没有类型推导,就不可能是通用引用。 2:声明形式必须是纯 T&& 引用的声明必须严格是 “T&...
Item25:对右值引用使用move,对通用引用使用forward
右值引用用 std::move,通用引用用 std::forward 右值引用(T&&,仅绑定右值):只能绑定 “可以被移动” 的对象(比如临时对象、被 std::move 转换的左值),你能确定这个对象的所有权可以被转移。 通用引用(T&&,模板中):也叫转发引用,可能绑定左值(如局部变量)或右值(如临时对象),无法提前确定是否能移动。 基于这两个特性,正确用法是: 右值引用:无条件用std::move转换为右值(因为它绑定的对象一定可以移动)。示例(Widget 的移动构造函数): 12345678910class Widget {public: Widget(Widget&& rhs) // rhs是右值引用 : name(std::move(rhs.name)), // 无条件移动 p(std::move(rhs.p)) {}private: std::string name; std::shared_ptr<int> p;}; ...
Item26:避免重载通用引用
通用引用太 “贪婪”,会抢走所有重载匹配 通用引用是模板 T&&,对任何类型实参都能精确匹配; 普通重载(如 int、const std::string&)需要类型转换 / 提升时,永远赢不过通用引用。 例子: 12void logAndAdd(int idx); // 普通重载template<typename T> void logAndAdd(T&& name); // 通用引用 传 short 时: 本想走 int 重载(需要类型提升) 结果通用引用精确匹配,直接抢走调用,导致后续无法转成 string,编译报错。 完美转发构造函数会 “劫持” 拷贝构造写一个带通用引用构造函数的类: 123456class Person {public: template<typename T> Person(T&& n); // 通用引用构造 Person(int idx); Person(const Person& r...
Item27:熟悉通用引用重载的替代方法
前提Item26 已证明:通用引用 + 普通重载 = 必出问题(劫持调用、编译报错、破坏拷贝 / 移动构造、继承失效)。 本条款给出5 种合法替代方案,从简单到高级,覆盖所有场景。 5 种通用引用重载的替代方案1. 放弃重载,直接改函数名 做法:把重载函数改成不同名字(如logAndAddName/logAndAddNameIdx) 优点:最简单,无任何匹配风险 缺点:构造函数不能用(名字固定为类名),失去重载的语法便利 2. 退而求其次:传 const T& 做法:放弃通用引用,用 C++98 的常量左值引用 优点:代码简单、匹配稳定、无坑 缺点:效率低,无法利用移动语义、做不到完美转发 3. 按值传递(pass by value) 做法:形参直接传值,配合std::move初始化成员 1explicit Person(std::string n) : name(std::move(n)) {} 优点:无匹配冲突,代码简洁,兼顾拷贝 / 移动 缺点:比完美转发略低效(多一次移动 / 拷贝) ...
Item41:对于移动成本低且总是被拷贝的可拷贝形式参,考虑按值传递
为什么要重新审视 “传值”?传统上,要实现 “左值拷贝、右值移动” 有两种方式,但都有缺陷: 重载方案:写两个函数(const T& 处理左值、T&& 处理右值),代码冗余(两份声明 / 实现 / 维护),目标代码也会生成两份; 通用引用方案:用模板 T&& + std::forward,虽只需一个函数,但模板需放头文件(膨胀)、支持类型多导致目标代码多、编译错误晦涩,还可能匹配意外类型。 能否用一个函数、不依赖通用引用,同时实现左值拷贝、右值移动? 可以 —— 用 “按值传递 + 移动语义”,但需明确适用条件。 传值 + 移动语义1. 实现方式(以 addName 为例)12345678class Widget {public: void addName(std::string newName) { // 形参按值传递 names.push_back(std::move(newName)); // 函数内move进容器 }private: std::ve...
Item42:考虑使用置入代替插入
插入函数的隐含性能开销对于存储 std::string 的标准容器(如 std::vector<std::string>),使用插入函数(push_back/push_front/insert 等)添加元素时,存在隐含的临时对象构造与销毁开销。 示例: 12std::vector<std::string> vs;vs.push_back("xyzzy"); 实参是字符串字面量 const char[6],与容器元素类型 std::string 不匹配; 编译器会先创建一个临时 std::string 对象(第一次构造); 临时对象作为右值,传入 push_back(T&&) 重载版本,在容器内存中再构造一次元素(移动构造,第二次构造); 函数返回后,临时对象被销毁(调用析构函数)。 一次插入操作,触发两次构造 + 一次析构,产生不必要的性能损耗。 置入函数的优势:就地构造,无临时对象C++11 引入的置入函数(emplace_back/emplace_front/emplac...
Item31:避免使用默认捕获模式
lambda 表达式:代码中的匿名函数片段(比如std::find_if中[](int val){ return 0<val && val<10; }这行),是编译期的代码片段; 闭包:lambda 在运行时创建的对象,持有捕获数据的副本 / 引用(比如传给find_if的第三个实参就是闭包); 闭包类:编译器为每个 lambda 自动生成的唯一类,闭包是该类的实例,lambda 的逻辑会变成闭包类成员函数的执行代码。 避免使用 lambda 的默认捕获模式C++11 有两种默认捕获模式:[&](按引用捕获)、[=](按值捕获) 1. 默认按引用捕获([&]):悬空引用风险 原理:按引用捕获会让闭包持有 “lambda 定义作用域内的局部变量 / 形参” 的引用;如果闭包的生命周期超过这些变量(比如闭包被存入容器,变量随函数返回销毁),引用就会变成悬空引用,后续使用闭包会触发未定义行为。 典型例子: 123456void addDivisorFilter() { int divisor = co...
