乐于分享
好东西不私藏

面试宝典(五):现代 C++ 特性与模板

面试宝典(五):现代 C++ 特性与模板

目录

  1. 从 C++98 到 C++20:为什么要学现代 C++
  2. 右值引用与 move 语义
  3. std::move
     与 std::forward:移动与完美转发
  4. 移动构造/移动赋值与"被移动的对象的契约"
  5. 值类别:左值、右值、纯右值、将亡值
  6. auto
     与 decltype 类型推导
  7. 范围 for 与初始化列表
  8. lambda 表达式:闭包的本质
  9. nullptr
    、强类型枚举、 constexpr
  10. 模板基础:函数模板、类模板、特化
  11. 偏特化与 std::enable_if / SFINAE
  12. CRTP:静态多态
  13. 可变参数模板与折叠表达式
  14. C++20 Concepts:模板约束的救世主
  15. 面试真题与速记

1. 从 C++98 到 C++20:为什么要学现代 C++

C++11 是"C++ 的第二语言",引入 auto、lambda、右值引用、智能指针、 constexpr、线程库、nullptr 等;C++14 完善泛型 lambda、make_unique;C++17 加入 std::optional/variant/string_view/filesystem/if constexpr/structured bindings;C++20 带来 concepts协程modulesranges。现代 C++ 目标是:资源管理自动化(RAII + 智能指针)、零成本抽象、表达力强、少写样板。面试中能熟练使用现代特性并讲清底层原理,是高级岗的硬指标。


2. 右值引用与 move 语义

左值(lvalue):有名字、有地址、可取 & 的表达式(如变量 int x;*p)。右值(rvalue):临时对象、字面量、将亡值,无持久身份(如 42func() 返回值、x+y)。

右值引用 T&&:只能绑定到右值,是 move 语义的基础。

为什么需要 move? 深拷贝大对象(如 std::vector 扩容、函数返回大对象)代价高。move 是"偷"资源(转移指针)而非复制,代价 O(1)。

#include<vector>#include<iostream>std::vector<intmake(){    std::vector<intv(1000000);    return v;                 // C++11 起:NRVO/move,不深拷贝}intmain(){    std::vector<int> a = make(); // 移动构造(偷内部缓冲区)}

关键std::vector 的移动构造只复制三个指针(指向数据、大小、容量)并把源对象置空,O(1) 而非 O(n)。


3. std::move 与 std::forward:移动与完美转发

  • std::move
    :只是一个强制类型转换,把左值/右值变成右值引用(static_cast<T&&>),本身不移动任何东西。真正的"移动"发生在移动构造/赋值里。
  • std::forward<T>
    :完美转发,在模板中按原值类别(左值仍是左值、右值仍是右值)转发参数。
#include<utility>#include<string>voidprocess(std::string&& s)/* 接管 s 的资源 */ }template <typename T>voidrelay(T&& arg){              // 通用引用(universal reference)    process(std::forward<T>(arg)); // 保持 arg 原本的左/右值属性}intmain(){    std::string a = "hello";    relay(a);          // T=string&,转发为左值    relay(std::string("tmp")); // T=string,转发为右值}

通用引用(universal/forwarding reference)T&& 出现在类型推导上下文(模板参数或 auto&&)时,既能绑左值也能绑右值,左值实例化出 T&,右值实例化出 T&&。这是完美转发的前提。

经典面试题std::move 之后对象处于什么状态?答:处于"有效但未指定(valid but unspecified)"状态——可以析构或赋值,但不应再使用其值(除非重新赋值)。标准只保证可安全销毁/赋值。


4. 移动构造/移动赋值与契约

#include<cstring>class Buffer {    char* data; size_t len;public:    Buffer(Buffer&& o) noexcept            // 移动构造        : data(o.data), len(o.len) {        o.data = nullptr; o.len = 0;       // 源置空,防止析构时 double free    }    Buffer& operator=(Buffer&& o) noexcept { // 移动赋值        if (this != &o) {            delete[] data;                 // 释放自己的旧资源            data = o.data; len = o.len;            o.data = nullptr; o.len = 0;        }        return *this;    }    ~Buffer() { delete[] data; }};

重点

  • 移动操作应标记为 noexcept,否则 std::vector 在扩容时因怕移动抛异常会退化为拷贝(影响性能)。
  • 移动后析构必须安全(源已置空)。
  • 编译器生成的移动操作:若用户定义了析构/拷贝/赋值,则不自动生成移动操作(经典陷阱)。

5. 值类别精解

C++11 把表达式按"身份(identity)+ 可移动性"分类:

  • lvalue
    :有身份、不可移动(可被取地址)。
  • xvalue(将亡值)
    :有身份、可移动(如 std::move(x)static_cast<T&&>(x))。
  • prvalue(纯右值)
    :无身份、可移动(临时对象、42a+b 结果)。
  • glvalue
     = lvalue + xvalue;rvalue = prvalue + xvalue。

理解这张表是答好"值类别"题的关键。std::move 把 lvalue/xvalue 变成 xvalue;std::forward 保持类别。


6. auto 与 decltype 类型推导

  • auto
    :从初始化表达式推导(忽略引用/顶层 const,除非 auto&/const auto&)。
  • decltype(expr)
    :返回 expr 的声明类型,保留引用和 const
  • decltype(auto)
    :结合两者,完美保留类别(用于转发返回)。
int x = 0;int& r = x;auto a = r;          // a 是 int(引用被脱掉)decltype(r) b = x;   // b 是 int&(保留引用)decltype(auto) c = r;// c 是 int&

注意坑auto 在 std::vector<bool> 上是特化,返回代理对象而非 bool&,别踩。范围 for 用 auto& 避免拷贝;只读用 const auto&;要移动用 auto&&


7. 范围 for 与初始化列表

std::vector<int> v = {123};          // 初始化列表for (const auto& x : v) std::cout << x;   // 范围 for

范围 for 底层等价于 auto it = begin(v); ... *it; ++it;,因此自定义类型只要提供 begin()/end()(返回迭代器)即可用。


8. lambda 表达式:闭包的本质

lambda 是匿名函数对象(闭包),编译器生成一个带 operator() 的匿名类。捕获列表决定它"吸"了哪些变量。

int base = 10;auto f = [base](int x) mutable { return x + base++; }; // 值捕获(拷贝),mutable 才能改副本auto g = [&base](int x) { return x + base; };          // 引用捕获// 泛型 lambda(C++14):参数用 autoauto h = [](auto a, auto b) { return a + b; };// 完美转发捕获(C++14 用 init-capture)auto p = [y = std::move(some_str)]() { return y; };

捕获方式

  • []
    :不捕获;[=]:值捕获全部;[&]:引用捕获全部。
  • [this]
    :捕获 this 指针(注意悬空!lambda 活得比对象久会 UB)。
  • 推荐显式列出要捕获的变量,避免 [&] 误引用已销毁局部变量。

与 std::function:lambda 可隐式转 std::function(类型擦除,有开销);作为模板参数直接传 lambda 类型零开销。std::function 本质是堆分配的小对象 + 虚调用,高频路径慎用。


9. nullptr、强类型枚举、constexpr

  • nullptr
    :类型安全的空指针,类型是 std::nullptr_t,解决 NULL/0 在重载歧义(如 void f(int) vs void f(char*) 传 NULL 可能选 int)。
  • enum class(强类型枚举)
    :作用域限定(Color::Red)、不隐式转 int、可指定底层类型,避免污染与歧义。
  • constexpr
    :编译期求值。constexpr 函数若参数是编译期常量,则结果在编译期算出(如数组大小、模板参数)。C++14 起 constexpr 函数体内可有循环/变量。
constexpr int sq(int x) { return x * x; }static_assert(sq(5) == 25"");   // 编译期int arr[sq(3)];                   // 用作数组大小

const vs constexprconst 是运行期只读;constexpr 是编译期常量(隐含 const)。面试能区分两者即过关。


10. 模板基础:函数模板、类模板、特化

模板是 C++ 泛型与零成本抽象的核心。

template <typename T>max(T a, T b)return a > b ? a : b; }   // 函数模板template <typename T, size_t N>class Array { T data_[N]; };                // 类模板// 全特化:针对特定类型定制template <>class Array<bool8> { /* 位压缩实现 */ };

模板实例化:编译器按使用的具体类型生成代码(代码膨胀风险)。typename 在嵌套依赖类型前必须写(如 typename T::value_type),这是语法要求而非风格。


11. 偏特化与 std::enable_if / SFINAE

偏特化(partial specialization):类模板可针对部分参数特化(函数模板不支持偏特化,用重载/enable_if 解决)。

SFINAE(Substitution Failure Is Not An Error):模板参数替换失败不算错误,只是该重载被剔除。这是编译期"类型分发"的经典技术。

#include<type_traits>#include<iostream>// 仅当 T 是整型才启用template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>void process(T v) { std::cout << "int: " << v << "\n"; }// 仅当 T 是浮点template <typename T, typename = std::enable_if_t<std::is_floating_point_v<T>>>void process(T v) { std::cout << "float: " << v << "\n"; }

注意上面两个函数模板"第二个默认参数相同"会冲突,实际要用 std::enable_if 放在返回类型或额外模板参数上区分。正确写法:

template <typename T>std::enable_if_t<std::is_integral_v<T>> process(T v) { /* 整型版 */ }template <typename T>std::enable_if_t<std::is_floating_point_v<T>> process(T v) { /* 浮点版 */ }

面试意义:老派"编译期条件分支"全靠 SFINAE,理解它能读懂 STL/Boost 源码。但 C++20 推荐用 concepts 替代(更清晰)。


12. CRTP:静态多态

CRTP(Curiously Recurring Template Pattern):基类以派生类为模板参数,实现编译期多态,避免虚函数运行时开销。

template <typename Derived>struct Base {    voidinterface() {        static_cast<Derived*>(this)->impl(); // 静态分发,无虚表    }};struct D : Base<D> {    voidimpl() { /* 具体实现 */ }};

应用std::enable_shared_from_this<T>boost::iterator_facadeEigen 表达式模板、计数对象个数(Counter<D>)、静态接口约束。面试能说出"编译期多态替代虚函数、无虚表开销"即加分。


13. 可变参数模板与折叠表达式

C++11 可变参数模板让函数接受任意数量/类型参数,配合递归展开或 ... 展开。C++17 折叠表达式更优雅。

#include<iostream>// C++17 折叠表达式:打印所有参数template <typename... Args>voidprint(Args&&... args){    (std::cout << ... << args) << "\n";   // 一元左折}// 求和template <typename... Nums>autosum(Nums... n)return (n + ...); }   // 折叠intmain()print(1" + "2" = "3); }

std::forward + 可变参数 实现完美转发工厂:

template <typename T, typename... Args>std::unique_ptr<T> make_unique(Args&&... args){    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));}

应用std::tuplestd::make_sharedfmt/std::format、日志库变参接口。能手写递归展开(基例 + 递归)是面试常考。


14. C++20 Concepts:模板约束的救世主

Concepts 让模板约束可读、报错友好,取代晦涩的 SFINAE。

#include<concepts>template <std::integral T>   // T 必须整数add(T a, T b)return a + b; }template <typename T>concept Addable = requires(T a, T b) { a + b; }; // 自定义概念template <Addable T>sum(T a, T b)return a + b; }

对比 SFINAE:旧代码约束一长串 enable_if 嵌套,报错如同天书;concepts 直接在接口处写约束,编译错误精准指向"类型不满足概念"。面试若能在现代特性题里展示 concepts 优先于 SFINAE,体现对 C++20 的掌握。

requires 表达式还能约束表达式的有效性、异常规范、返回类型(配合 std::same_as 等),实现精细接口契约。


15. 面试真题与速记

速记清单

  • 右值引用 T&& 支撑 move;std::move 仅转换不移动,move 发生在构造/赋值。
  • std::forward
     完美转发,配合通用引用 T&&(推导上下文)。
  • move 操作标 noexcept;移动后源对象"有效但未指定"。
  • 值类别:lvalue / xvalue / prvalue;glvaluervalue 是其并集。
  • auto
     脱引用/顶层 const;decltype 保留类别。
  • lambda = 闭包类;[=] 值、[&] 引用;[this] 注意悬空。
  • nullptr
     类型安全;enum class 强类型;constexpr 编译期。
  • 模板:偏特化仅类模板;SFINAE 实现编译期分发;CRTP 静态多态;可变参数+折叠表达式。
  • C++20 concepts 取代 SFINAE,约束清晰、报错友好。

高频真题

  1. std::move
     真的移动了吗?(没,只是转右值引用,移动在构造/赋值里发生)
  2. 为什么移动构造要 noexcept?(vector 扩容怕异常退化为拷贝)
  3. 通用引用和右值引用区别?(T&& 在推导上下文是通用引用,可绑左右值)
  4. lambda 捕获 [=] 和 [&] 区别?this 捕获坑?(引用捕获活过局部变量会悬空)
  5. 什么是 SFINAE?现在用什么替代?(模板替换失败非错;C++20 concepts)
  6. CRTP 有什么用?(编译期多态,无虚表开销)
  7. auto
     推导规则?std::vector<bool> 的坑?(代理引用)
  8. const
     与 constexpr 区别?(运行期只读 vs 编译期常量)
  9. 写一个可变参数求和(折叠表达式或递归展开)。
  10. 怎么判断一个类型是否可调用/有某成员?(concepts requires 表达式 / SFINAE decltype 探测)

小结

现代 C++ 的内核是零成本抽象 + 资源管理自动化 + 强类型约束。右值引用与 move 让值传递不再昂贵,模板与 concepts 把"编译期计算与约束"变成日常工具,lambda 与 auto 大幅减少样板。高级工程师的标志,是不仅能用这些特性,还能讲清它们背后的类型系统与编译期机制——这正是本篇的考察重心。

16. 补充:std::optional / variant / any(C++17 三剑客)

C++17 引入三个"现代替代裸指针/union/void*"的类型,显著提升安全性与表达力:

  • std::optional<T>
    :表示一个"可能有值也可能无值"的对象,替代"用空指针/特殊值表示不存在"。无堆分配,值语义,避免 nullptr 解引用。
#include<optional>std::optional<intfind(int key){    if (key == 0return std::nullopt// 无值    return key * 2;}if (auto v = find(3); v) std::cout << *v;
  • std::variant<Ts...>:类型安全的联合体(tagged union),替代 C 风格 union(C 的 union 不跟踪当前类型,易误用)。配合 std::visit 做编译期分派(见 STL 篇 std::variant 替代虚函数示例)。

  • std::any:可存任意可拷贝类型的单值容器,类型擦除;需 std::any_cast 取出(类型不符抛异常)。适合插件/脚本桥接等"运行时才知类型"场景,但比 variant 慢且有堆分配。

面试点:能用 optional 表达"失败"比用异常更轻、比特殊值更清晰;variant+visit 是"用值语义 + 编译期多态替代继承"的现代手法。


17. 补充:std::string_view 与零拷贝字符串处理

std::string_view(C++17)是对已有字符串的只读视图(指针 + 长度),不拥有内存、不拷贝。用于函数参数避免不必要的 string 拷贝(尤其是子串切分)。

#include<string_view>voidlog(std::string_view s)/* 只读,不拷贝 */ }   // 接受 string / char* / 子串std::string big = "hello world";log(big);                      // 无拷贝log(std::string_view(big).substr(0,5)); // 切分也无拷贝

陷阱string_view 不拥有底层内存,若指向的 string 被销毁/修改,string_view 悬空——绝不要返回指向局部 string 的 string_view,也不要长期持有。面试能指出"视图的生命周期必须短于被视图者"是安全关键。


18. 补充:结构化绑定与 if constexpr

结构化绑定(C++17):一行解包 tuple/pair/结构体成员,写代码更清爽(map 遍历 for (auto& [k,v] : m) 就是它)。

if constexpr(C++17):编译期条件分支,满足条件的分支参与实例化、不满足的直接丢弃(不要求语法合法)。这是模板里取代大量 SFINAE 的利器。

template <typename T>autoprocess(T v){    ifconstexpr(std::is_pointer_v<T>)        return *v;          // T 是指针才实例化这行    else        return v;           // T 不是指针才实例化这行}

对比传统 SFINAE,if constexpr 阅读与维护成本大幅降低,是 C++17 模板编程的转折点。面试能同时展示 SFINAE(老)与 if constexpr/concepts(新)并说明取舍,体现经验厚度。


19. 真题演练(现代 C++)

Q1:std::move 一个 const 对象会发生什么? A:std::move(const T) 得到 const T&&(右值引用但带 const),会匹配拷贝构造而非移动构造(因为移动构造通常接受 T&& 非 const)。所以"移动 const 对象"实际是拷贝——这是常见反模式,说明不该把 const 对象当资源源。

Q2:auto 推导 std::vector<bool>::reference 的坑? A:vector<bool> 特化为位压缩,operator[] 返回的是代理类型(不是 bool&)。auto x = v[0]; 拿到的是代理对象,后续修改 v[0] 不会反映到 x;应写 bool x = v[0]; 或 auto&& x = v[0];。这是 auto + 代理引用最著名的坑。

Q3:模板特化与重载哪个优先级高? A:非模板函数 > 特化模板 > 主模板(在同样匹配时);重载决议先看参数匹配度,模板特化不参与重载决议而只参与"选了哪个模板后再选哪个特化"。要精确控制分发,常把特化写成重载或用 if constexpr/concepts 代替,避免"特化不参与重载"的困惑。

Q4:constexpr 函数能在运行时调用吗? A:能。constexpr 函数既可在编译期(参数都是常量时)求值,也可在运行期当普通函数用——它不强制编译期。要强制编译期求值用 consteval(C++20);要运行时也可但允许编译期用 constexprconstinit(C++20)保证变量是编译期初始化、运行期不变。

Q5:为什么建议所有重写都加 override A:编译器会校验基类确有该虚函数且签名匹配;若手误写成 virtual void sound(int)(参数错了)没加 override,会被当成新函数而非重写,多态悄然失效且毫无报错——这是隐蔽 bug。override 把这类错误变成编译错误。

20. 深度专题:模板编译模型与代码膨胀

模板是"按需实例化":只用到的具体类型才会生成代码。这带来零成本抽象(只为实际使用的类型付费),但也可能导致代码膨胀(code bloat)——同样的算法为 int、long、double… 各生成一份。

  • 缓解膨胀
    :把与类型无关的逻辑抽到非模板基类/自由函数(“非类型相关部分去模板化”),用 shared_impl 公共实现 + 薄模板壳。
  • 显式实例化
    template class Vector<int>; 把实例化限制在一个 TU,避免每个编译单元都生成(配合 extern template 声明抑制重复实例化,加快编译)。
  • 头文件代价
    :模板多在头文件,改动触发大面积重编;用 PIML/隐式接口、模块化(C++20 modules)缓解。
// 头文件抑制实例化extern template classstd::vector<int>;  // 别在当前 TU 实例化// 某 .cpp 里:template classstd::vector<int>; // 集中实例化一次

面试点:能权衡"泛型灵活 vs 编译时间/二进制体积",并知道 extern template、模块化等手段,是大型项目经验。


21. 真题再加码(现代 C++ 终极)

Q1:auto 和 decltype 在 decltype(auto) 里怎么配合? A:decltype(auto) 让返回类型精确沿用表达式的 decltype 类别(保留引用/值类别)。典型用于完美转发返回:decltype(auto) f() { return expr; } 既可能返回引用也可能返回值,由 expr 决定。对比 auto 会脱掉引用导致意外拷贝。面试能讲清"想保留引用时用 decltype(auto)"。

Q2:C++20 三件套(concepts / ranges / modules)各自解决什么? A:concepts 给模板加可读约束、报错友好(替代 SFINAE);ranges 提供惰性、可组合、管道式的算法(如 views::filter | views::transform),表达力强且无中间容器;modules 替代 #include 文本包含,消除宏污染、加速编译、改善封装。import std; 取代 #include <vector>。面试能列出三者价值体现对现代标准的掌握。

Q3:std::forward 和 std::move 能互相替代吗? A:不能。move 无条件转右值(丢弃值类别信息);forward<T> 按 T 推导结果条件地转左/右值(保持原类别)。转发万能引用参数必须用 forward,否则右值被当成左值导致意外拷贝;而 move 用于"我明确要搬走这个对象"。混用会破坏完美转发语义。

Q4:lambda 可以递归吗?怎么写? A:可以。[&] auto self = [&](int n){ return n<=1?1:n*self(n-1); };(C++14 泛型 lambda + 按引用捕获自身)或 std::function 包装。注意按值捕获自身会拷贝,需 mutable 或用 std::ref。面试能写出"按引用捕获自身实现递归 lambda"是细节。

Q5:constexpr 函数里能用 new/delete 吗? A:C++20 起 constexpr 允许动态分配(且必须在编译期释放,不能泄漏到运行期);C++17 及之前不允许。所以能写编译期计算的动态结构(如编译期可变长容器)。面试能指出"版本差异"体现对标准的时效掌握。

Q6:为什么说"现代 C++ 尽量不用裸 new"? A:裸 new 容易漏 delete、异常路径难释放。现代做法:用 std::make_unique/make_shared(异常安全、一次分配)、用容器(vector/string)管理连续内存、用 std::pmr 做池化、用 std::string_view 避免拷贝。裸 new 只留给底层库/分配器作者。面试能升华到"RAII 优先、所有权清晰"是工程成熟标志。

22. 高频速答 20 题(现代 C++ 闪电战)

  1. 右值引用作用?
     支撑 move 语义,转移资源而非拷贝。
  2. std::move 真移动了吗?
     没,仅转右值引用,移动发生在构造/赋值。
  3. std::forward 用途?
     完美转发,保持参数原值类别。
  4. 通用引用 vs 右值引用?T&&
     在推导上下文是通用引用,可绑左右值。
  5. 为何移动构造标 noexcept
     防 vector 扩容遇异常退化为拷贝。
  6. 移动后对象状态?
     valid but unspecified,可析构/赋值。
  7. 值类别三类?
     lvalue / xvalue / prvalue。
  8. auto 推导规则?
     脱引用/顶层 const;引用需 auto&
  9. decltype vs decltype(auto)
     前者保留类别;后者精确沿用表达式类别。
  10. lambda 本质?
     匿名闭包类,含 operator()
  11. [=] vs [&]
     值捕获拷贝、引用捕获绑定。
  12. [this] 坑?
     lambda 活得比对象久会悬空。
  13. nullptr 优于 NULL
     类型安全(std::nullptr_t),无重载歧义。
  14. const vs constexpr
     运行期只读 vs 编译期常量。
  15. SFINAE 是什么?
     模板替换失败非错,用于编译期分发。
  16. C++20 替代 SFINAE?
     concepts,约束清晰报错友好。
  17. CRTP 作用?
     编译期多态,无虚表开销。
  18. 可变参数模板核心?
     递归展开或 ... 折叠(C++17)。
  19. optional 用途?
     表达"可能有值",替代空指针/特殊值。
  20. string_view 注意?
     不拥有内存,生命周期必须短于被视图者。

23. 临场速记清单(现代 C++ 篇面试前过一遍)

  • 右值引用 T&& 支撑 move;std::move 仅转换不移动,移动发生在构造/赋值。
  • std::forward
     完美转发,配合通用引用 T&&(推导上下文)保持值类别。
  • move 操作标 noexcept;移动后源对象"valid but unspecified",可析构/赋值。
  • 值类别:lvalue / xvalue / prvalue;glvaluervalue 为其并集。
  • auto
     脱引用/顶层 const;decltype 保留类别;decltype(auto) 精确沿用。
  • lambda = 闭包类;[=] 值、[&] 引用;[this] 注意悬空;递归用按引用捕获自身。
  • nullptr
     类型安全;enum class 强类型;constexpr 编译期常量(C++20 可动态分配)。
  • 模板:偏特化仅类模板;SFINAE 实现编译期分发;C++20 concepts 替代 SFINAE。
  • CRTP 静态多态(无虚表开销);可变参数模板 + 折叠表达式处理任意参数。
  • optional
    /variant/string_view 是现代替代裸指针/union/拷贝的利器。
  • if constexpr
     编译期分支、丢弃不满足分支,取代大量 SFINAE。
  • 现代 C++ 铁律:尽量不用裸 new,用 make_unique/容器/RAII,所有权清晰。

24. 补充:现代 C++ 易错 Top 6(避坑必看)

  1. 移动后继续用对象值
    std::move 后对象处于 valid but unspecified 状态,再用其值(除非重新赋值)是隐患。
  2. 通用引用没用 forward
    T&& 参数不 std::forward 转发出去,右值会被当左值导致意外拷贝。
  3. auto 接 vector<bool>::reference
    :拿到的是代理引用而非 bool&,悬空/失效导致诡异 bug。
  4. lambda [this] 捕获后对象先析构
    :lambda 生命周期长于对象时 this 悬空,应捕获成员副本或 weak_ptr 守卫。
  5. 模板特化以为参与重载决议
    :特化只在"已选定主模板后"生效,不进入重载排序,分发错误应改用重载/if constexpr/concepts。
  6. const 对象被 move
    std::move(const T) 匹配拷贝而非移动,资源没搬走,是隐蔽的反模式。

25. 一句话口诀(现代 C++ 篇)

  • 右值引用撑 move,move 只转不搬。
  • forward
     保类别,universal reference 配它。
  • move 标 noexcept,移动后别用值。
  • auto
     脱 const,decltype 留类别。
  • lambda 是闭包,[=] 值 [&] 引用。
  • nullptr
     类型安全,enum class 强类型。
  • constexpr
     编译期,concepts 替 SFINAE。
  • CRTP 静态多态,无虚表开销。
  • optional
    /variant/string_view 现代三件宝。
  • 现代 C++ 少用裸 new,RAII 管所有权。

26. 补充:边界追问三连

  • Q:C++20 协程的三个关键字?
     A:co_await(挂起等待)、co_yield(产出值并挂起)、co_return(协程返回);底层靠 promise_type / awaitable 协作,是标准库/框架(如 cppcoro)构建异步的基础。
  • Q:modules 相比 #include 好处?
     A:模块化编译消除文本包含带来的宏污染、重复解析与"包含爆炸",显著加速编译、改善封装与接口边界。import std; 取代一堆 #include
  • Q:什么时候不该用模板?
     A:当类型集合固定且较少、可读性/编译速度更优先时,虚函数 + 接口更直观;模板适合"需要零成本抽象、类型参数化且类型无限"的场景,否则编译慢、报错难。

27. 收尾两点提醒

  • 不要为了现代而现代
    move/shared_ptr/模板不是越多越好。能 unique_ptr 就别 shared_ptr,能编译期 if constexpr 就别运行时分支,能值语义就别裸指针。可读性、可维护性优先,性能瓶颈处再用"重型武器"。
  • 理解而非背诵
    :面试官深挖时(“为什么移动构造要 noexcept”“concepts 怎么替代 SFINAE”“虚函数为什么阻止内联”),考察的是你能否讲清底层机制。把本篇的机制串成"类型系统 + 编译期 + 零成本抽象"一条主线,比记结论更有说服力。