第七章 值类别、移动语义与模板基础
第七章 值类别、移动语义与模板基础
1. 左值、右值、纯右值和将亡值分别是什么?
问题分析
这道题考查是否建立了值类别、模板实例化与编译期约束的清晰概念边界,并能把类别、职责和限制对应起来。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:每个表达式都有类型和值类别。
- 再讲机制:围绕“分类体系 → 边界”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合值类别、模板实例化与编译期约束说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:腾讯客户端一面
问题讲解
一、分类体系
每个表达式都有类型和值类别。glvalue 表示具有身份、可定位对象的表达式,包含 lvalue 和 xvalue;rvalue 表示可被移动的值,包含 prvalue 和 xvalue。lvalue 通常有身份且不应被隐式窃取;prvalue 常用于初始化结果对象;xvalue 有身份但资源可被复用,如 std::move(x)。
xvalue 同时属于 glvalue 和 rvalue:它仍指向具有身份的对象,同时允许后续操作把该对象视为资源可复用的来源。两套分类不是互斥划分。
二、边界
有名字的变量表达式始终是左值,即使变量类型是 T&&;要把它恢复为可转发的值类别需 std::move 或 std::forward。值类别属于表达式,不属于对象本身。
三、常见错误
- 用“在赋值号左边”定义左值。
- 认为右值引用变量在表达式中仍是右值。
- 把临时对象一律说成没有地址。
- 将值类别与 const 混为一谈。
回答自检
- 两组分类的交叉关系。
- 身份与资源可复用性的含义。
- xvalue 为何同时属于两组。
- 有名字的右值引用变量为何是左值。
面试官可能追问的问题
- 字符串字面量是什么? 是 const 字符数组左值。
- 函数返回
T&的调用表达式是什么? 左值。 - 函数返回
T&&呢? 将亡值。
2. std::move 到底做了什么?
问题分析
这道题考查能否用值类别、模板实例化与编译期约束解释题目中的语义,而不是停留在定义记忆。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:
std::move(x)近似执行到右值引用的转换,把表达式标记为可复用资源的 xvalue。 - 再讲机制:围绕“真实作用”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合值类别、模板实例化与编译期约束说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:美团 C++ 一面
问题讲解
一、真实作用
std::move(x) 近似执行到右值引用的转换,把表达式标记为可复用资源的 xvalue。真正的资源转移发生在随后被调用的移动构造或移动赋值中;若类型没有移动操作、对象是 const,仍可能调用拷贝。
标准库类型移动后除另有约定外有效但值未指定;用户类型须查其移动契约,不能把标准库的一般要求套给任意类型。unique_ptr 成功移动构造后为空,而 string 不应一概画成空。不要对还会读取的对象过早 move,也不要对局部返回值机械 move,因为可能阻碍 NRVO。
图以unique_ptr成功移动构造为例;不能据此认为所有类型移动后都为空。
二、常见错误
- 认为调用
std::move立即清空源对象。 - 从 const 对象期待高效移动。
return std::move(local);机械优化。- 移动后依赖未由类型文档保证的值。
回答自检
- move 是转换而非搬运。
- 后续重载决议如何选择移动操作。
- moved-from 对象的有效性边界。
- const 和返回值场景中的限制。
面试官可能追问的问题
- 为什么 const 对象通常不能移动资源? 移动往往需要修改源对象,而参数
const T&&不允许。 - 基本类型 move 有意义吗? 通常等同复制。
- 何时应显式 move? 将命名对象所有权交给接收方且之后不再依赖原值时。
3. 移动构造与拷贝构造应该如何选择?
问题分析
这道题不只是让你罗列名词,而是考查能否从值类别、模板实例化与编译期约束做出有依据的比较和选择。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:源是左值且调用者还要保留原值时复制;
- 再讲机制:围绕“选择规则”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合值类别、模板实例化与编译期约束说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:百度后端一面
问题讲解
一、选择规则
源是左值且调用者还要保留原值时复制;源是临时值或明确放弃的对象时移动。移动适合拥有堆缓冲区、句柄等可转移资源的类型;固定小对象移动可能与复制成本相同。类型是否可复制、可移动由业务语义决定。
容器扩容为维持强异常保证,常在移动构造可能抛异常且复制可用时选择复制。因此资源类型的正确移动构造通常应 noexcept。
本图假设该资源类型的移动契约使源对象为空,不代表所有类型的一般保证。
二、常见错误
- 认为移动一定 O(1);小字符串优化或内联数组可能仍复制内容。
- 为不可复制资源实现“复制后共享”,改变语义。
- 移动构造忘记使源对象可安全析构。
- 无条件声明 noexcept 而内部成员移动会抛。
回答自检
- 选择取决于源值是否保留。
- 移动效率依赖类型结构。
- moved-from 不变式。
- noexcept 与容器异常保证的联系。
面试官可能追问的问题
- 只声明移动会怎样? 可能使隐式复制操作被删除。
- vector 为什么关心 noexcept? 扩容失败时需要保持旧元素不被破坏。
- 移动构造能否被省略? 可以,拷贝省略也适用于本可移动场景。
4. 什么是返回值优化和 C++17 强制拷贝省略?
问题分析
这道题考查是否建立了值类别、模板实例化与编译期约束的清晰概念边界,并能把类别、职责和限制对应起来。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:RVO 通常指返回临时结果直接在调用方目标位置构造;
- 再讲机制:围绕“两类情形”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合值类别、模板实例化与编译期约束说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:字节基础架构一面
问题讲解
一、两类情形
RVO 通常指返回临时结果直接在调用方目标位置构造;具名局部变量的 NRVO 是允许但非强制的优化。C++17 对某些同类型 prvalue 初始化规定直接构造结果对象,不再要求先创建临时再移动,因此即使复制/移动构造被删除也可合法。
return std::move(local) 通常让表达式不再满足 NRVO 形式,反而可能多一次移动。可通过编译器选项观察,但不能用日志次数定义程序正确性。
二、常见错误
- 把所有返回优化都称为 C++17 强制。
- 认为 NRVO 不发生时一定复制,不考虑移动。
- 为“帮助优化”显式 move 局部返回值。
- 依赖构造函数副作用发生固定次数。
回答自检
- RVO、NRVO 与强制省略的区别。
- prvalue 直接构造结果对象。
return std::move(local)的反效果。- 正确性为何不能依赖拷贝次数。
面试官可能追问的问题
- 返回函数参数能 NRVO 吗? 参数不是 NRVO 的具名局部候选,通常移动/复制。
- 不同分支返回不同局部变量呢? NRVO 往往无法统一目标,具体看结构和优化。
- 为什么 C++17 能返回不可移动类型? 对规定场景直接构造结果对象。
5. 什么是完美转发?std::forward 为什么需要模板参数?
问题分析
这道题考查能否从可观察现象追溯到值类别、模板实例化与编译期约束中的具体机制,并说明规则被破坏后的后果。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:包装函数的具名参数在函数体内都是左值,直接传下去会丢失调用者的值类别。
- 再讲机制:围绕“问题与机制”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合值类别、模板实例化与编译期约束说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:腾讯客户端二面
问题讲解
一、问题与机制
包装函数的具名参数在函数体内都是左值,直接传下去会丢失调用者的值类别。形如模板推导上下文中的 T&& 是转发引用:传左值时 T 推导为左值引用,传右值时推导为非引用。std::forward<T>(arg) 根据 T 恢复原始值类别。
它需要模板参数 T,因为仅观察具名变量 arg 无法知道调用点原本是左值还是右值。T&& 只有发生该形式的类型推导时才是转发引用,普通 Widget&& 只是右值引用。
二、完整可执行示例
同一个 relay 分别接收左值和右值。若把 std::forward<T>(value) 改成 value,两次调用都会进入左值重载,因为具名参数表达式本身是左值。
#include <iostream>
#include <string>
#include <utility>
void consume(const std::string&) {
std::cout << "lvalue overload\n";
}
void consume(std::string&&) {
std::cout << "rvalue overload\n";
}
template <class T>
void relay(T&& value) {
consume(std::forward<T>(value));
}
int main() {
std::string text = "C++17";
relay(text); // 保留左值属性
relay(std::string("temporary")); // 保留右值属性
}

三、常见错误
- 在非模板右值引用上称“万能引用”。
- 对同一参数多次 forward,第一次可能已移动资源。
- 用 move 替代 forward,错误地移动左值。
- 忽略初始化列表、重载函数等推导失败场景。
回答自检
- 具名参数为何变成左值表达式。
- 转发引用的推导规则。
- forward 如何依赖 T 恢复值类别。
- move 与 forward 的职责差异。
面试官可能追问的问题
- 引用折叠规则核心是什么? 只要组合中有
&,结果通常为&,只有&& + &&为&&。 - 为什么工厂函数使用 forward? 将构造实参属性原样传给目标构造函数。
- C++20 如何简化? 可用
auto&&参数或概念约束,但机制仍基于推导。
6. 函数模板和类模板解决什么问题?
问题分析
这道题考查能否用值类别、模板实例化与编译期约束解释题目中的语义,而不是停留在定义记忆。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:函数模板描述一族函数,模板实参通常可从调用参数推导;
- 再讲机制:围绕“两类模板”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合值类别、模板实例化与编译期约束说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:美团 C++ 一面
问题讲解
一、两类模板
函数模板描述一族函数,模板实参通常可从调用参数推导;类模板描述一族类型,传统上需显式给出实参,C++17 支持类模板实参推导。编译器在使用点代入模板实参、检查语义并生成所需实例。
模板比 void* 和宏保留类型信息,支持重载、约束和优化。代价包括错误信息复杂、编译时间增加以及多个实例可能带来代码体积。
二、常见错误
- 把模板当成运行期多态。
- 认为所有实例都会生成代码;未使用成员可能不实例化。
- 用宏完成本可类型检查的泛型操作。
- 假设不同模板实例是同一类型。
回答自检
- 模板描述的是函数族或类型族。
- 推导、替换、实例化的基本过程。
- 模板与宏、虚函数的区别。
- 泛型代码的收益与成本。
面试官可能追问的问题
- 函数模板能重载吗? 可以,也可与非模板函数共同参与重载。
- 类模板参数能是值吗? 可以,非类型模板参数常用于数组长度。
- 模板会膨胀代码吗? 可能,具体取决于实例数、链接合并和优化。
7. 为什么模板定义通常放在头文件中?什么是显式实例化?
问题分析
这道题考查能否从可观察现象追溯到值类别、模板实例化与编译期约束中的具体机制,并说明规则被破坏后的后果。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:每个翻译单元独立编译。
- 再讲机制:围绕“可见性原因”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合值类别、模板实例化与编译期约束说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:百度 C++ 一面
问题讲解
一、可见性原因
每个翻译单元独立编译。当源码使用 Box<int> 时,编译器通常必须看到模板完整定义才能生成实例,仅有声明不足。因此模板定义常放在头文件,并依赖 ODR 合并等机制处理多个等价实例。
可在一个 .cpp 写显式实例化定义 template class Box<int>;,其他翻译单元用 extern template class Box<int>; 抑制重复实例化。这样能缩短编译,但只适合预先知道并固定的类型集合。
二、常见错误
- 把模板定义放 cpp,却期望任意外部类型都能实例化。
- 认为 inline 只用于运行时内联。
- 显式实例化遗漏实际需要的成员或类型。
- 在不同翻译单元提供不一致模板定义,违反 ODR。
回答自检
- 翻译单元独立编译的影响。
- 使用点为何需要模板定义。
- 显式实例化和 extern template 的配合。
- 集中实例化的适用限制。
面试官可能追问的问题
- 模板声明和定义能分文件吗? 可以,但定义仍需在实例化点可见,常用
.tpp被头文件包含。 - 显式特化也必须在头文件吗? 声明需在使用前可见,定义位置取决于实体和 ODR。
- C++20 module 能改善吗? 可改善依赖组织,但模板实例化可见性问题仍需模块接口正确导出。
8. C++20 concepts 和 C++17 SFINAE 有什么关系?
问题分析
这道题不只是让你罗列名词,而是考查能否从值类别、模板实例化与编译期约束做出有依据的比较和选择。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:SFINAE 表示替换模板实参时某候选形成无效类型或表达式,不立即让整个编译失败,而是从重载集合移除。
- 再讲机制:围绕“共同目标与差异”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合值类别、模板实例化与编译期约束说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:腾讯 C++ 基础架构二面
问题讲解
一、共同目标与差异
SFINAE 表示替换模板实参时某候选形成无效类型或表达式,不立即让整个编译失败,而是从重载集合移除。C++17 常用 enable_if、void_t 和检测惯用法。C++20 Concepts/requires 将约束提升为语言机制,可命名概念、组合约束并参与更清晰的偏序。
Concepts 不会让模板自动变成运行时接口,也不验证函数体内所有语义。约束应描述调用者可理解的能力,而不是具体成员名堆砌。
二、常见错误
- 把所有模板编译错误都称为 SFINAE。
- 在函数体内触发错误后期待候选自动移除。
- 用概念过度约束到某个具体实现。
- 在 C++17 主答案中混用 requires 语法。
回答自检
- 替换失败与普通编译错误的边界。
- C++17 检测惯用法的用途。
- Concepts 如何显式表达约束。
- 两者与重载决议的联系。
面试官可能追问的问题
if constexpr是 SFINAE 吗? 不是,它在已选模板实例内部丢弃分支。- Concepts 会减少生成代码吗? 主要改善约束与诊断,不必然减少实例化代码。
- 何时继续用 SFINAE? 必须兼容 C++17 时。
9. 模板全特化和偏特化有什么区别?
问题分析
这道题不只是让你罗列名词,而是考查能否从值类别、模板实例化与编译期约束做出有依据的比较和选择。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:全特化为一组完整确定的模板实参提供实现,例如
Traits<bool>。 - 再讲机制:围绕“两种定制”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合值类别、模板实例化与编译期约束说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:美团基础架构二面
问题讲解
一、两种定制
全特化为一组完整确定的模板实参提供实现,例如 Traits<bool>。偏特化为一类更具体的参数模式提供实现,例如 Traits<T*>。类模板和变量模板可偏特化;函数模板不能偏特化,通常通过函数重载实现类似选择。
多个偏特化都匹配时,编译器选择更特化者;无法确定则二义。标准库模板通常只允许在明确授权情形下为用户自定义类型做特化,不能任意为内建类型改标准行为。
二、常见错误
- 尝试偏特化函数模板。
- 特化声明放在错误命名空间。
- 在某些使用已实例化后才声明特化。
- 滥用标准库特化造成未定义行为。
回答自检
- 完整实参与参数模式的区别。
- 哪些模板支持偏特化。
- 函数重载为何替代函数偏特化。
- 标准库特化的限制。
面试官可能追问的问题
- 函数模板如何按类型类别定制? 使用重载、约束或 tag dispatch。
- 全特化还是模板吗? 它是特定实参的显式特化实体,不再进行同样的参数推导。
- 偏特化如何排序? 按哪个匹配集合更窄、更特化决定。
10. 可变参数模板和折叠表达式如何工作?
问题分析
这道题考查能否从可观察现象追溯到值类别、模板实例化与编译期约束中的具体机制,并说明规则被破坏后的后果。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:可变参数模板用类型参数包和函数参数包接收任意数量参数,包展开把模式应用到每个元素。
- 再讲机制:围绕“参数包与展开”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合值类别、模板实例化与编译期约束说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:字节 C++ 后端二面
问题讲解
一、参数包与展开
可变参数模板用类型参数包和函数参数包接收任意数量参数,包展开把模式应用到每个元素。C++17 折叠表达式用一个运算符合并参数包,替代许多递归模板。
#include <iostream>
template <class... Ts>
auto sum(Ts... values) {
return (0 + ... + values);
}
int main() {
std::cout << sum(1, 2, 3, 4) << '\n';
}
对非结合运算,左折叠和右折叠结果可能不同。空参数包是否有效取决于运算符是否有语言定义单位元,或是否提供显式初值。
二、常见错误
- 展开参数时丢失值类别,应配合
forward<Ts>(args)...。 - 假设任意运算符左右折叠等价。
- 没考虑空包。
- 在一个展开表达式中制造不清晰的副作用顺序。
回答自检
- 类型包、函数包与包展开。
- 四种折叠形式的核心差异。
- 初值与空包的关系。
- 转发参数包时如何保留值类别。
面试官可能追问的问题
sizeof...(Ts)返回什么? 参数包元素数量。- 如何逐个执行表达式? C++17 可使用逗号折叠。
- 递归模板还需要吗? 某些异构结构递归仍有用,但简单聚合优先折叠表达式。
11. C++ 模板元编程是什么?有什么用途?
问题分析
这道题考查是否建立了值类别、模板实例化与编译期约束的清晰概念边界,并能把类别、职责和限制对应起来。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:模板元编程利用模板实例化、类型特征、特化和常量表达式,在编译期根据类型或值生成不同代码。
- 再讲机制:围绕“它把什么工作提前了? → 收益与代价”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合值类别、模板实例化与编译期约束说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:百度基础架构二面(编辑化整理)
问题讲解
一、它把什么工作提前了?
模板元编程利用模板实例化、类型特征、特化和常量表达式,在编译期根据类型或值生成不同代码。常见用途是选择实现、检查前置条件和计算固定配置;它不是运行期反射,也不应为了炫技把简单逻辑写成递归模板。
#include <iostream>
#include <type_traits>
template <class T>
int size_category(const T&) {
if constexpr (std::is_arithmetic_v<T>) return 1;
return 2;
}
int main() {
std::cout << size_category(3) << ' ' << size_category("text") << '\n';
}

二、收益与代价
模板元编程能把不合法调用变成编译错误,并常常让分支在编译期消失;代价是编译时间、错误信息和代码体积可能增加。C++17 优先使用 if constexpr、std::void_t 和已有类型特征,C++20 则用 Concepts 表达接口约束。
三、常见错误
- 把模板实例化当成运行期动态分派。
- 递归模板没有终止条件,导致编译失败。
- 用模板替代本来一行普通函数就能表达的逻辑。
回答自检
- 模板元编程的输入、选择和生成过程。
- 编译期收益与编译成本之间的取舍。
if constexpr、类型特征和 Concepts 的关系。
面试官可能追问的问题
- 模板元编程生成的代码一定更快吗? 不一定,仍要看实例数量、优化和数据访问。
if constexpr为什么能放不同类型代码? 不满足条件的分支在该实例中被丢弃。- 如何限制模板参数? C++17 可用 SFINAE,C++20 优先 Concepts。
12. CRTP 是什么?适合什么场景?
问题分析
这道题考查是否建立了值类别、模板实例化与编译期约束的清晰概念边界,并能把类别、职责和限制对应起来。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:CRTP 让派生类把自己作为模板参数传给基类:
Base<Derived>。 - 再讲机制:围绕“核心结构 → 适用边界”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合值类别、模板实例化与编译期约束说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:字节基础架构二面(编辑化整理)
问题讲解
一、核心结构
CRTP 让派生类把自己作为模板参数传给基类:Base<Derived>。基类可以在编译期把调用转给 Derived,从而获得静态多态,不需要虚表和运行时类型判断。
#include <iostream>
template <class Derived>
class Printable {
public:
void print() const {
static_cast<const Derived&>(*this).print_impl();
}
};
class Number : public Printable<Number> {
public:
void print_impl() const { std::cout << 42 << '\n'; }
};
int main() { Number{}.print(); }

二、适用边界
CRTP 适合编译期固定的策略、静态接口检查、混入类和避免重复代码。它不能替代需要运行期替换实现的虚函数接口,也会增加模板实例化和类型耦合。基类中的 static_cast 只有在继承关系和模板参数约定正确时才安全。
| 维度 | CRTP | 虚函数多态 |
|---|---|---|
| 分派时机 | 编译期 | 运行期 |
| 类型集合 | 编译期固定 | 可运行期扩展 |
| 典型成本 | 可能内联,无虚表 | 间接调用和对象模型成本 |
| 代价 | 模板耦合、代码膨胀 | 需要稳定基类接口 |
三、常见错误
- 把 CRTP 说成真正的运行时多态。
- 通过错误的
static_cast访问不属于派生类的对象。 - 为了消除一次虚调用引入过度复杂的模板层次。
回答自检
- CRTP 如何通过模板参数获得静态分派。
- 它和虚函数多态的适用边界。
static_cast安全依赖的继承约定。
面试官可能追问的问题
- CRTP 能否保存
Base<Derived>*并混用不同 Derived? 不同实例是不同类型,不能直接当成统一运行时接口。 - CRTP 一定没有成本吗? 可能增加代码体积和编译时间,数据布局成本也不能忽略。




