⚡C++ 语言基础

C++17 与 C++20 核心特性

难度:⭐⭐ | 高频指数:🔥🔥

面试回答

常见问法

  • C++17 有哪些你用过的新特性?
  • 结构化绑定是什么?怎么用?
  • if constexpr 和普通 if 有什么区别?
  • std::optionalstd::variantstd::string_view 分别解决什么问题?
  • C++20 的 concepts 是什么?和 SFINAE 有什么关系?
  • std::span 和 ranges 你了解多少?

回答

C++17 和 C++20 引入了大量实用特性,面试时不需要全部精通,但要能说出”知道哪些、用过哪些、解决什么问题”。

C++17 核心特性:

  1. 结构化绑定(Structured Bindings):可以用 auto [a, b] = ... 的方式一次性解包 pair、tuple、struct 的成员,代码更简洁。

  2. if constexpr:编译期条件分支,不满足条件的分支不会被实例化,在模板编程中替代了大量 SFINAE 技巧。

  3. std::optional<T>:表示”可能有值也可能没有”,替代用 -1nullptr 表示无效值的做法。

  4. std::variant<T...>:类型安全的联合体,替代 union + 手动类型标记。

  5. std::string_view:只读的字符串引用,不拥有内存,避免不必要的 std::string 拷贝。

C++20 核心特性:

  1. Concepts:给模板参数加约束,编译错误信息更友好,替代 SFINAE 的”正统方案”。

  2. std::span<T>:连续内存的非拥有视图,类似 string_viewstring 的关系,但面向数组/vector。

  3. 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::variantunion 的区别? variant 是类型安全的,它知道当前存的是哪个类型,访问错误类型会抛异常(std::get)或返回 nullptr(std::get_if)。union 没有这种保护。

5. Concepts 和 SFINAE 的关系? Concepts 是 C++20 提供的”正规约束语法”,本质上替代了 SFINAE 的大部分使用场景。优势是:语法清晰、错误信息友好、可组合。SFINAE 仍然有效,但新代码优先用 concepts。


原理展开

1. 结构化绑定的本质

结构化绑定不是”解构赋值”那么简单。编译器实际上会创建一个匿名变量来持有整个对象,然后 ab 是对这个匿名变量成员的引用。

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::variantstd::get<T> 在类型不匹配时会抛 std::bad_variant_access,生产代码建议用 std::get_if
  • Concepts 的 requires 表达式只检查语法合法性,不检查语义正确性。
  • std::spanstring_view 一样有生命周期问题,不要返回指向局部变量的 span。

记忆技巧

  • C++17 三件套optional(可能没有)、variant(多选一)、string_view(只读借用)

  • C++17 语法糖:结构化绑定(解包)、if constexpr(编译期 if)

  • C++20 三件套:concepts(约束模板)、span(内存视图)、ranges(管道算法)

  • 一句话区分

    • optional = 有或没有
    • variant = 是 A 还是 B
    • string_view / span = 借来看看,不拥有
  • 面试答题模板

    1. 说出特性名称和解决的问题
    2. 给一个最简代码示例
    3. 提一个注意事项或陷阱
    4. 如果能和旧方案对比更好

面试速答版

C++17 我比较常用的特性有:结构化绑定,可以用 auto [a, b] 解包 pair 和 struct;if constexpr 在模板中做编译期分支消除;std::optional 表示可能为空的返回值;std::string_view 做零拷贝的字符串引用。C++20 方面,concepts 给模板加约束,比 SFINAE 可读性好很多;std::span 统一了连续内存的接口;ranges 支持管道式的惰性算法组合。这些特性的共同方向是让代码更安全、更可读、减少模板元编程的复杂度。

Related · 语言基础