搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第32章 C++17/20核心特性

第32章 C++17/20核心特性

高级 👤 已掌握C++11/14,希望编写更现代、更安全、更高效代码的开发者 🔧 构建新项目、重构旧代码、提升代码表达力与性能
本章你将学到
  • 结构化绑定、if/switch初始化语句等简化代码的语法糖
  • std::optional、std::variant等表达“可能无值”或“多选一”的安全类型
  • C++20的核心概念:概念约束、三路比较、范围for的初始化
核心概念结构化绑定std::optional概念(Concepts)

如果你还在用C++11/14的语法写新项目,就像开着老式手动挡汽车上高速——能跑,但费劲。C++17和C++20带来的不仅是新功能,更是一种思维转变:让编译器帮你做更多检查,让代码意图更清晰,让常见错误在编译期就暴露。我在重构一个遗留的日志解析模块时,引入std::optional和结构化绑定,代码行数减少了30%,逻辑却一目了然。

C++17:让日常编码更轻松

C++17的许多特性都是为了消除样板代码。想象一下拆快递包裹,以前你需要用剪刀小心地划开胶带,再一层层拿出物品(对应老式的std::tie)。现在有了结构化绑定,就像包裹自带拉链,一拉就开,所有东西整齐呈现。

结构化绑定 (Structured Bindings)

它允许你从数组、结构体或std::tuple/pair中一次性解包多个值到独立的变量中。

// 传统方式:繁琐且类型声明冗长
std::map<int, std::string> m = {{1, "one"}};
auto iter = m.find(1);
if (iter != m.end()) {
    int key = iter->first;        // 手动解包
    std::string value = iter->second;
}

// C++17结构化绑定:清晰直接
if (auto [key, value] = m.find(1); iter != m.end()) {
    // 直接使用 key 和 value
    std::cout << key << ": " << value << std::endl;
}

// 同样适用于结构体和std::tuple
struct Point { double x, y; };
Point p{3.14, 2.71};
auto [x_coord, y_coord] = p; // x_coord=3.14, y_coord=2.71

这里有个隐藏的坑:结构化绑定声明的变量是“绑定”到原对象的成员或元素的引用,对于auto [x, y] = point;这种按值捕获,修改x不会影响point.x。如果是auto& [x, y] = point;,那么修改x就是修改point.x。

if/switch语句中的初始化器

这个特性彻底解决了“作用域污染”问题。以前为了在条件判断中使用一个临时变量,你不得不把它声明在外部,导致其作用域过大。

// 旧写法:result的作用域是整个函数
auto result = some_computation();
if (result.is_valid()) {
    // 使用result
} else {
    // 这里也能访问result,可能误用
}

// C++17新写法:result的作用域仅限于if和else块
if (auto result = some_computation(); result.is_valid()) {
    // 使用result
} else {
    // 这里也能访问result,但逻辑上更合理
    log_error(result.error_code());
}
// result在这里已不可见

常见误区

  • 滥用[[maybe_unused]]:这个属性用于抑制编译器关于未使用变量的警告。但如果你发现需要频繁使用它,更应该检查代码设计,而不是简单压制警告。它应该是最后的手段。
  • 误解std::string_view的生命周期:std::string_view不拥有数据,它只是一个“视图”。如果你用一个临时std::string构造了一个string_view,然后临时字符串被销毁,这个string_view就变成了悬垂引用,访问它是未定义行为。我在项目中踩过这个坑,一个解析函数返回了string_view,但调用者把它存起来长期使用,导致间歇性崩溃。
  • 认为std::optional可以替代指针:std::optional<T>存储的是T的值,不是指针。这意味着T必须是可复制/移动的。对于大对象或不可移动对象,使用std::optional<std::unique_ptr<T>>可能更合适。

更安全的数据表达:std::optional与std::variant

实际开发中,很多错误源于对“无值”或“多种可能类型”的拙劣表达。用-1表示错误?用空指针表示不存在?用联合体(union)手动管理类型?这些方法都容易出错。C++17提供了类型安全的解决方案。

std::optional<T>

  • 场景:函数可能失败,找不到元素,配置项可选。
  • 替代:返回特殊值(-1, nullptr),输出参数,布尔状态+结果引用。
  • 用法:if (auto val = find_value()) { use(*val); }

std::variant<T1, T2...>

  • 场景:JSON节点(可能是字符串、数字、数组),网络消息包,状态机的不同状态数据。
  • 替代:继承体系,带标签的联合体,void*指针。
  • 用法:配合std::visit访问,类型安全。

std::any

  • 场景:需要存储任意类型,类型在运行时确定(如插件系统、脚本引擎)。
  • 警告:类型安全成本高,应作为最后选择。
  • 用法:if (val.type() == typeid(int)) { int i = std::any_cast<int>(val); }

来看一个std::variant的实战例子,它比传统的枚举+联合体安全得多:

// 表示一个配置项,可能是整数、浮点数或字符串
using ConfigValue = std::variant<int, double, std::string>;

void process_config(const ConfigValue& cv) {
    // std::visit 接受一个可调用对象和variant
    std::visit(overloaded {
        [](int i)   { std::cout << "整数: " << i << std::endl; },
        [](double d) { std::cout << "浮点数: " << d << std::endl; },
        [](const std::string& s) { std::cout << "字符串: " << s << std::endl; }
    }, cv);
}

// 需要为`overloaded`提供一个简单的实现(C++17)
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; // 推导指引

编者提示:使用std::variant和std::visit时,其模式类似于“访问者模式”,如果你对设计模式感兴趣,可以回顾第30章《设计模式基础》中关于行为型模式的讲解,能帮助你更好地理解其设计思想。

C++20:迈向现代模板编程与简洁比较

C++20的规模堪比C++11,其中“概念(Concepts)”是改变游戏规则的特性。它让模板错误信息从几十行“天书”变成一句人话。如果你看到类似“在‘requires’子句中的约束不满足”这样的错误,大概率是你的模板实参不符合概念要求。

模板函数/类
→
概念约束 (Concepts)
→
编译器检查
↓
清晰错误信息
←
类型安全

概念 (Concepts) —— 给模板参数加上“说明书”

生活类比:就像网购时的筛选器(“支持无线充电”、“防水等级IP68”),概念是给模板类型参数加的筛选条件,不符合条件的类型根本进不来。技术定义:概念是编译期的谓词,用于约束模板参数,它指定了类型必须满足的一组要求。

// 1. 使用标准库概念 (定义于<concepts>和<iterator>等头文件)
template<std::integral T> // 约束T必须是整数类型
T square(T x) { return x * x; }

// 尝试用double调用:编译错误,信息明确!
// square(3.14); // 错误:'square': 未满足关联约束

// 2. 自定义概念
template<typename T>
concept Drawable = requires(T t) {
    { t.draw() } -> std::same_as<void>; // 要求有返回void的draw()成员函数
};

template<Drawable D> // 使用自定义概念
void render(const D& obj) {
    obj.draw();
}

三路比较运算符 (Spaceship Operator) <=>

为自定义类型实现完整的比较操作(==, !=, <, <=, >, >=)曾经需要写6个函数,现在只需要一个<=>。编译器会自动生成其余5个。

三路比较运算符返回类型与含义
返回类型 (定义于<compare>)含义示例 (a <=> b)
std::strong_ordering::equala 等于 b1 <=> 1
std::strong_ordering::lessa 小于 b1 <=> 2
std::strong_ordering::greatera 大于 b2 <=> 1
std::partial_ordering::unordereda 与 b 不可比(如NaN)NaN <=> 1.0
struct Point {
    int x, y;
    
    // 自动生成 ==, !=, <, <=, >, >=
    auto operator<=>(const Point& other) const = default;
    
    // 或者自定义词典序比较
    // auto operator<=>(const Point& other) const {
    //     if (auto cmp = x <=> other.x; cmp != 0) return cmp;
    //     return y <=> other.y;
    // }
};

int main() {
    Point p1{1, 2}, p2{1, 3};
    std::cout << boolalpha;
    std::cout << (p1 < p2) << std::endl;  // true,编译器自动生成
    std::cout << (p1 != p2) << std::endl; // true,编译器自动生成
}

范围for的初始化语句:这是个小而美的特性,允许在范围for循环中初始化一个变量,其生命周期仅限于该循环。

for (auto vec = get_some_vector(); auto& x : vec) {
    // 处理x,vec在这个循环结束后销毁
}
// vec在这里已不可见,避免污染外部作用域

动手试一试

  1. 重构练习:找一个你以前写的函数,它可能通过返回bool表示成功,并通过输出参数返回结果。尝试用std::optional重写它,观察接口如何变得更清晰。
  2. 概念约束:写一个模板函数print_range,它接受一个范围并打印所有元素。使用std::ranges::range概念(C++20)来约束模板参数,确保它只能接受范围类型。
  3. 三路比较:为你自定义的一个简单类(比如Book,包含title(字符串)和id(整数)成员)添加operator<=>,并测试所有6种比较操作。

检验你的理解

  1. 下面代码中,sv在process函数返回后还能安全使用吗?为什么?
    std::string_view get_suffix(const std::string& str) {
        if (str.length() > 5) {
            return std::string_view(str.data() + str.length() - 5, 5);
        }
        return "";
    }
    void process() {
        auto sv = get_suffix(std::string("hello world"));
        std::cout << sv << std::endl;
    }
  2. 假设有一个std::variant<int, std::string> v,当前包含字符串"test"。执行int i = std::get<int>(v);会发生什么?有没有更安全的访问方式?
  3. 概念(Concepts)的主要作用是什么?它是在编译期还是运行期起作用?

本章小结

  • 结构化绑定和if/switch初始化器是C++17的语法糖,能显著减少样板代码,限制变量作用域,提升代码清晰度。
  • std::optional, std::variant, std::any提供了类型安全的方式来表达“可选值”、“多选一”和“任意类型”,替代容易出错的特殊值、联合体和裸指针。
  • C++20概念(Concepts)革命性地改善了模板编程,通过约束模板参数在编译期提供清晰错误信息,并作为编写泛型代码的核心工具。
  • 三路比较运算符(<=>)简化了自定义类型的比较操作实现,编译器可自动生成完整的比较运算符集。
  • 时刻注意新特性的生命周期与陷阱,如std::string_view的悬垂引用风险,这是在实际项目中必须警惕的。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注