10、C++17/20新特性:结构化绑定、std::optional、std::variant、std::any、折叠表达式、协程入门

好,咱们今天聊点实在的。C++11之后,语言一直在进化。到了C++17和C++20,很多新特性已经不是「锦上添花」,而是能直接改变你写代码的方式。我刚开始接触这些特性时,说实话有点抗拒——总觉得老一套够用了。但后来在项目里踩了几次坑,才明白这些新东西是真的能救命。

这一章,我带你过一遍我认为最实用的几个特性。结构化绑定、std::optionalstd::variantstd::any、折叠表达式,还有协程入门。每个我都会结合自己的经验来讲,该避的坑一个不落。

结构化绑定:让代码少写一半废话

先说说结构化绑定。说白了,就是让你能从元组、结构体、数组里一次性拆出多个变量。以前你得写:

auto t = std::make_tuple(42, "hello", 3.14);
int a = std::get<0>(t);
std::string b = std::get<1>(t);
double c = std::get<2>(t);

现在一行搞定:

auto [a, b, c] = std::make_tuple(42, "hello", 3.14);

干净利落。我个人习惯在写map遍历时大量使用它:

std::map<int, std::string> m = {{1, "one"}, {2, "two"}};
for (const auto& [key, val] : m) {
    std::cout << key << ": " << val << "\n";
}
小提示:结构化绑定默认是拷贝。如果你不想拷贝,记得加引用:auto& [k, v]。我在项目中遇到过因为忘记加引用导致性能下降的情况,尤其是处理大对象时。

std::optional:优雅地处理「可能有值」

以前我们怎么表示「可能没有值」?用指针、用特殊值(比如-1)、或者传一个bool引用。都不够优雅。std::optional就是专门干这个的。

std::optional<int> findValue(int id) {
    if (id > 0) return id * 2;
    return std::nullopt;  // 表示没有值
}

auto result = findValue(10);
if (result) {
    std::cout << "找到了: " << *result << "\n";
} else {
    std::cout << "没找到\n";
}

你想想看,这比返回一个裸指针安全多了。我曾经在一个遗留系统里看到大量返回nullptr的函数,调用方忘了检查就直接崩了。换成std::optional,至少语义上就告诉你「这玩意儿可能没有」。

注意:不要滥用std::optional。如果函数返回的是「错误信息」,应该用std::expected(C++23)或异常。optional只适合「有值/无值」这种二元状态。

std::variant:类型安全的联合体

std::variant是C++17带来的类型安全版union。它能在同一内存位置存储不同类型,但不会像union那样让你乱来。

std::variant<int, double, std::string> v;
v = 42;                    // 存int
v = 3.14;                  // 存double
v = "hello"s;              // 存string

// 访问值
if (auto* p = std::get_if<int>(&v)) {
    std::cout << "int: " << *p << "\n";
} else if (auto* p = std::get_if<double>(&v)) {
    std::cout << "double: " << *p << "\n";
}

更优雅的方式是用std::visit

std::visit([](auto&& arg) {
    std::cout << arg << "\n";
}, v);

嗯,这里要注意:std::variant不能存储引用类型,也不能存储数组。我在项目中用它来替代一些「多态」场景,比虚函数轻量得多。

std::any:万能容器,但别滥用

std::any可以存储任意类型的值。听起来很美好,但代价是类型擦除和运行时检查。

std::any a = 42;
a = std::string("hello");
a = 3.14;

// 取出时必须知道类型
try {
    auto val = std::any_cast<double>(a);
    std::cout << val << "\n";
} catch (const std::bad_any_cast& e) {
    std::cerr << "类型不匹配: " << e.what() << "\n";
}
警告:我个人建议尽量少用std::any。它破坏了类型安全,而且性能开销不小。如果你需要存储多种类型,优先考虑std::variant。只有在你确实不知道运行时类型、且类型数量不确定时,才考虑std::any

折叠表达式:让变参模板更简洁

折叠表达式是C++17对变参模板的语法糖。以前你要递归展开参数包,现在一行搞定。

// 求和
template<typename... Args>
auto sum(Args... args) {
    return (args + ...);  // 一元右折叠
}

// 打印所有参数
template<typename... Args>
void printAll(Args... args) {
    (std::cout << ... << args) << "\n";  // 二元左折叠
}

std::cout << sum(1, 2, 3, 4, 5) << "\n";  // 输出15
printAll("a", "b", "c");  // 输出abc

我曾经在写日志库时大量使用折叠表达式,配合std::apply,可以写出非常灵活的格式化输出。你想想看,以前要写递归模板特化,现在一行代码就解决了。

协程入门:异步编程的新范式

协程是C++20引入的重磅特性。它允许你暂停和恢复函数的执行,非常适合异步I/O、生成器、惰性求值等场景。

先看一个最简单的生成器例子:

#include <coroutine>
#include <iostream>

struct Generator {
    struct promise_type {
        int current_value;
        auto get_return_object() { return Generator{*this}; }
        auto initial_suspend() { return std::suspend_always{}; }
        auto final_suspend() noexcept { return std::suspend_always{}; }
        void unhandled_exception() { std::terminate(); }
        auto yield_value(int value) {
            current_value = value;
            return std::suspend_always{};
        }
        void return_void() {}
    };

    struct iterator {
        Generator& gen;
        bool operator!=(const iterator&) const { return !gen.finished; }
        iterator& operator++() {
            gen.handle.resume();
            if (gen.handle.done()) gen.finished = true;
            return *this;
        }
        int operator*() const { return gen.handle.promise().current_value; }
    };

    Generator(promise_type& p) : handle(std::coroutine_handle<promise_type>::from_promise(p)) {}
    ~Generator() { if (handle) handle.destroy(); }

    iterator begin() {
        handle.resume();
        return iterator{*this};
    }
    iterator end() { return iterator{*this}; }

    std::coroutine_handle<promise_type> handle;
    bool finished = false;
};

Generator range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;  // 暂停并返回值
    }
}

int main() {
    for (int v : range(0, 5)) {
        std::cout << v << " ";  // 输出 0 1 2 3 4
    }
}

说实话,协程的样板代码有点多。但核心思想很简单:co_yield暂停并返回值,co_await等待异步操作,co_return结束协程。

我的建议:初学者不要急着写协程框架。先用现成的库(比如cppcoro、boost.asio的协程支持)来感受一下。等理解了「暂停/恢复」的语义,再自己实现promise_type。

知识体系总览

下面这张图帮你理清这些新特性的定位和关系:

C++17/20 新特性核心 结构化绑定 从元组/结构体/数组 一次性拆出多个变量 auto [a, b] = t; std::optional 安全表示「可能有值」 替代指针/特殊值 if (opt) { use(*opt); } std::variant 类型安全的联合体 替代union和部分多态 std::visit(visitor, v); std::any 万能容器(慎用) 类型擦除,运行时检查 any_cast<T>(a); 折叠表达式 变参模板的语法糖 一行搞定参数包展开 (args + ...); 协程(C++20) 暂停/恢复函数执行 异步I/O、生成器 co_yield / co_await 核心原则:优先用variant,其次optional,最后考虑any 协程适合异步场景,折叠表达式简化模板元编程

避坑指南

最后,分享几个我踩过的坑:

  • 结构化绑定与引用:如果你写auto [a, b] = func(),a和b是拷贝。要引用必须显式写auto&。我曾经因为这个在循环里修改了副本而不是原数据,排查了半天。
  • std::optional与bool转换:if (opt)检查的是是否有值,不是值本身。如果你存的是bool,注意区分opt.has_value()*opt
  • std::variant的默认构造:默认构造会初始化第一个类型。如果第一个类型没有默认构造函数,variant就无法默认构造。我建议总是显式初始化。
  • 协程的堆分配:协程框架默认会在堆上分配状态。如果你在性能敏感路径上使用,记得自定义分配器或使用noop_coroutine

好了,这一章的内容就到这里。这些新特性不是让你炫技的,而是实实在在能帮你写出更安全、更简洁的代码。多用用,你会爱上它们的。


公众号:蓝海资料掘金营,微信 deep3321