C++17 与 C++20 核心特性
难度:⭐⭐ | 高频指数:🔥🔥
面试回答
常见问法
- C++17 有哪些你用过的新特性?
- 结构化绑定是什么?怎么用?
if constexpr和普通if有什么区别?std::optional、std::variant、std::string_view分别解决什么问题?- C++20 的 concepts 是什么?和 SFINAE 有什么关系?
std::span和 ranges 你了解多少?
回答
C++17 和 C++20 引入了大量实用特性,面试时不需要全部精通,但要能说出”知道哪些、用过哪些、解决什么问题”。
C++17 核心特性:
-
结构化绑定(Structured Bindings):可以用
auto [a, b] = ...的方式一次性解包 pair、tuple、struct 的成员,代码更简洁。 -
if constexpr:编译期条件分支,不满足条件的分支不会被实例化,在模板编程中替代了大量 SFINAE 技巧。 -
std::optional<T>:表示”可能有值也可能没有”,替代用-1或nullptr表示无效值的做法。 -
std::variant<T...>:类型安全的联合体,替代union+ 手动类型标记。 -
std::string_view:只读的字符串引用,不拥有内存,避免不必要的std::string拷贝。
C++20 核心特性:
-
Concepts:给模板参数加约束,编译错误信息更友好,替代 SFINAE 的”正统方案”。
-
std::span<T>:连续内存的非拥有视图,类似string_view对string的关系,但面向数组/vector。 -
Ranges:管道式算法组合,
views::filter | views::transform这种写法,惰性求值。
// C++17 结构化绑定
auto [key, value] = std::pair{1, "hello"};
// C++17 if constexpr
template <typename T>
auto process(T val) {
if constexpr (std::is_integral_v<T>) {
return val * 2;
} else {
return val;
}
}
// C++17 std::optional
std::optional<int> findUser(int id) {
if (id == 42) return 42;
return std::nullopt;
}
// C++20 concepts
template <typename T>
concept Addable = requires(T a, T b) { a + b; };
template <Addable T>
T add(T a, T b) { return a + b; }
追问
1. 结构化绑定能绑定哪些类型?
三类:数组、tuple-like 类型(pair、tuple、自定义 get<>)、以及所有非静态成员都是 public 的聚合类型(struct)。
2. if constexpr 和 #if 预处理有什么区别?
if constexpr 是编译期语义,在模板实例化阶段决定走哪个分支,不走的分支不会被编译器检查类型正确性(但语法必须合法)。#if 是预处理阶段的文本替换,完全不同层次。
3. std::optional 和指针表示”可能为空”有什么区别?
optional 是值语义,不涉及堆分配和所有权问题;指针需要考虑谁负责释放、是否为空等。optional 语义更清晰,适合函数返回值。
4. std::variant 和 union 的区别?
variant 是类型安全的,它知道当前存的是哪个类型,访问错误类型会抛异常(std::get)或返回 nullptr(std::get_if)。union 没有这种保护。
5. Concepts 和 SFINAE 的关系? Concepts 是 C++20 提供的”正规约束语法”,本质上替代了 SFINAE 的大部分使用场景。优势是:语法清晰、错误信息友好、可组合。SFINAE 仍然有效,但新代码优先用 concepts。
原理展开
1. 结构化绑定的本质
结构化绑定不是”解构赋值”那么简单。编译器实际上会创建一个匿名变量来持有整个对象,然后 a、b 是对这个匿名变量成员的引用。
std::map<int, std::string> m = {{1, "one"}, {2, "two"}};
for (const auto& [k, v] : m) {
// k 是 const int&,v 是 const std::string&
std::cout << k << ": " << v << "\n";
}
注意:auto& 和 const auto& 会影响绑定的引用性质。如果用 auto(值),则是拷贝。
2. if constexpr 的编译期分支消除
if constexpr 最大的价值在模板中:
template <typename T>
std::string stringify(T val) {
if constexpr (std::is_same_v<T, std::string>) {
return val;
} else if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(val);
} else {
return "unknown";
}
}
如果没有 if constexpr,当 T = int 时,return val(string 分支)也会被编译器检查,导致编译错误。if constexpr 让不满足条件的分支被丢弃,不参与类型检查。
3. std::optional 的正确用法
std::optional<std::string> getConfig(const std::string& key) {
auto it = config_map.find(key);
if (it != config_map.end()) return it->second;
return std::nullopt;
}
// 使用方
auto val = getConfig("timeout");
if (val) {
std::cout << *val << "\n";
} else {
std::cout << "not found\n";
}
核心原则:用 optional 表示”合法的无值状态”,而不是用魔法值或异常。
4. std::variant 与访问者模式
using Result = std::variant<int, std::string, double>;
Result r = "hello";
// 用 std::visit + 重载 lambda
std::visit([](auto&& val) {
std::cout << val << "\n";
}, r);
std::variant 配合 std::visit 是类型安全的多态替代方案,不需要虚函数和继承。
5. C++20 Concepts 基本用法
// 定义 concept
template <typename T>
concept Sortable = requires(T a) {
{ a.begin() } -> std::input_or_output_iterator;
{ a.end() } -> std::input_or_output_iterator;
{ a.size() } -> std::convertible_to<std::size_t>;
};
// 使用 concept 约束模板
template <Sortable Container>
void sort_it(Container& c) {
std::sort(c.begin(), c.end());
}
Concepts 的三种使用方式:
template <Concept T>:约束模板参数requires子句:template <typename T> requires Concept<T>- 简写:
void f(Concept auto x)
6. std::span —— 连续内存的通用视图
void process(std::span<int> data) {
for (int x : data) { /* ... */ }
}
std::vector<int> v = {1, 2, 3};
int arr[] = {4, 5, 6};
process(v); // vector → span
process(arr); // 数组 → span
span 不拥有内存,只是一个 {pointer, size} 对。它统一了”接受连续内存”的函数接口。
7. Ranges 管道式写法
#include <ranges>
namespace rv = std::views;
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8};
auto result = nums
| rv::filter([](int x) { return x % 2 == 0; })
| rv::transform([](int x) { return x * x; });
for (int x : result) {
std::cout << x << " "; // 4 16 36 64
}
Ranges 的核心优势:
- 惰性求值:不会创建中间容器
- 可组合:管道式链接多个操作
- 可读性:比嵌套
std::transform+std::copy_if清晰
易错点
- 结构化绑定用
auto是拷贝,用auto&才是引用,遍历 map 时忘加&会导致不必要的拷贝。 if constexpr只在模板中有”分支消除”效果,在非模板函数中它和普通if没有本质区别。std::optional不适合表示错误,它只表示”有或没有”。错误处理应该用std::expected(C++23)或异常。std::string_view不拥有内存,如果原始字符串被销毁,string_view就悬垂了。std::variant的std::get<T>在类型不匹配时会抛std::bad_variant_access,生产代码建议用std::get_if。- Concepts 的
requires表达式只检查语法合法性,不检查语义正确性。 std::span和string_view一样有生命周期问题,不要返回指向局部变量的 span。
记忆技巧
-
C++17 三件套:
optional(可能没有)、variant(多选一)、string_view(只读借用) -
C++17 语法糖:结构化绑定(解包)、
if constexpr(编译期 if) -
C++20 三件套:concepts(约束模板)、
span(内存视图)、ranges(管道算法) -
一句话区分:
optional= 有或没有variant= 是 A 还是 Bstring_view/span= 借来看看,不拥有
-
面试答题模板:
- 说出特性名称和解决的问题
- 给一个最简代码示例
- 提一个注意事项或陷阱
- 如果能和旧方案对比更好
面试速答版
C++17 我比较常用的特性有:结构化绑定,可以用 auto [a, b] 解包 pair 和 struct;if constexpr 在模板中做编译期分支消除;std::optional 表示可能为空的返回值;std::string_view 做零拷贝的字符串引用。C++20 方面,concepts 给模板加约束,比 SFINAE 可读性好很多;std::span 统一了连续内存的接口;ranges 支持管道式的惰性算法组合。这些特性的共同方向是让代码更安全、更可读、减少模板元编程的复杂度。