【C++】C++17的主要特性

文章目录

一、结构化绑定

之前的C++中,函数返回多个值,我们一般需要这么写:

cpp 复制代码
auto result = get_user();  // 返回std::pair<std::string, int>
std::cout << result.first << " " << result.second;

first / second 这种名字,读代码时根本猜不出含义。遍历 map 更难受:

cpp 复制代码
for (const auto& kv : score)
    std::cout << kv.first << " = " << kv.second;

C++17带来了结构化绑定:结构化绑定是一个语法糖,它允许你从一个数组、结构体、元组或类似的拥有多个数据成员的对象中,一次性声明并初始化多个变量。这使得代码更简洁、更易读,特别是当你需要处理像std::pair 或 std::tuple 这样的类型时。​

  • 结构化绑定的变量数量必须与表达式中的元素数量匹配
  • 结构化绑定的变量生命周期与绑定的对象相同
  • 结构化绑定的变量是表达式中对应元素的拷贝或引用
  • 结构化绑定不能用于类的私有成员,除非在类的成员函数内部
  • 结构化绑定不支持嵌套绑定
cpp 复制代码
// 使用方法
auto [name, value] = get_user();
for (const auto& [name, score] : m) { ... }
cpp 复制代码
#include <iostream>
#include <map>
#include <string>
#include <tuple>

struct Point { int x; int y; };

int main()
{
    // 1. pair
    auto p = std::make_pair(1, 2.5);
    auto [a, b] = p;
    std::cout << a << " " << b << "\n";

    // 2. 聚合体(成员全是 public 的结构体)
    Point pt{3, 4};
    auto [x, y] = pt;
    std::cout << x << " " << y << "\n";

    // 3. 数组
    int arr[3] = {7, 8, 9};
    auto [i, j, k] = arr;
    std::cout << i << j << k << "\n";

    // 4. 遍历 map:最常用的场景
    std::map<std::string, int> score{{"Alice", 90}, {"Bob", 85}};
    for (const auto& [name, s] : score)
        std::cout << name << " = " << s << "\n";

    return 0;
}

二、inline 变量

在 C++17 之前,如果你想在头文件中定义一个变量(比如一个全局常量 int g_counter = 0;),你会遇到一个经典的难题:ODR(One Definition Rule,单定义规则)​

文件会被多个 .cpp 包含,每个 .cpp 编译成一个翻译单元(TU),于是每个 TU 里都有了一份 g_counter 的定义。链接的时候,链接器看到好几个同名的强符号,直接报重复定义。

那以前怎么绕?两条路,都有毛病:

cpp 复制代码
// 路一:加 static
static int g_counter = 0;   // 每个 TU 各自一份,改了这边那边看不见

// 路二:extern 声明 + 单独一个 .cpp 里定义
extern int g_counter;       // 头文件里只声明
int g_counter = 0;          // 某个 .cpp 里定义,容易被忘掉

路一的问题很致命:每个 TU 一份副本,根本不共享。路二能共享,但要在两个地方同步维护,改类型的时候特别容易漏。

C++17 让inline可以修饰变量了。**inline 变量的含义是:允许在多个翻译单元中定义相同的变量,并且保证这些定义指向的是同一个实体(同一个地址)。链接器会从多个定义中挑选一个,并丢弃其他的,从而确保整个程序中只有一个该变量的实例。

C++17之前,类的静态成员变量,要做声明和定义分离,相对麻烦。​

cpp 复制代码
// Cache.h
class Widget {
public:
	static std::string name; // 声明​
	static const int version = 1; // 仅对整型static const允许类内定义初始化​
};
// Cache.cpp
std::string Widget::name = "张三"; // 定义初始化​

C++17之后,使用inline,你可以在声明的同时直接初始化,所有定义相关的工作都在头文件中完成。这是inline变量最常用、最直观的应用场景,它彻底简化了类静态成员变量的初始化。​

cpp 复制代码
// Cache.h
class Widget {
public:
	inline static std::string name = "张三"; // 定义初始化​
	static const int version = 1; // 仅对整型static const允许类内定义初始化​
};

三、if / switch 初始化语句

C++17允许if和switch语句中声明和初始化变量:

cpp 复制代码
if (auto it = m.find("apple"); it != m.end())
    std::cout << it->second;

分号前面是初始化语句 ,后面才是条件。变量 it 的作用域只限制在 if/else 语句内,出了这个语句就用不了,编译器会直接报未声明。

switch 也支持同样的写法:

cpp 复制代码
switch (int v = get_level(); v) 
{ ... }

这种写法和结构化绑定配合最香:比如std::map::insert 返回 pair<iterator, bool>,正好可以一行写完并判断结果:

cpp 复制代码
if (auto [it, ok] = m.insert({"x", 1}); ok)
    std::cout << "inserted " << it->first << "\n";

初始化语句里做结构化绑定是合法的,实测输出 inserted x。这种写法比"先 insert、再取 first、再判断 second"清爽太多。

四、属性

属性[[...]] 这套语法是 C++11 引入的,属性是为代码实体(变量、函数、类等)添加额外信息的一种机制,这些信息可以帮助编译器进行优化、生成警告或错误,以及指导静态分析工具。C++14/C++17/C++20中属性语法逐渐丰富

  • [[noreturn]] (C++11),包宿函数不会返回到它的调用点,主要用于优化和错误检测。通过明
    确告知编译器函数不会返回,编译器可以优化代码逻辑,例如跳过某些无效的后续代码执行等。标
    记为noreturn 的函数返回时,编译器会提示警告。
  • [[carries_dependency]](C++11),高级并发属性,它是为专家级并发优化提供的工具,但对于大多数的开发者来说,使用 std::atomic 默认内存序或其他高级并发工具是更明智的选择。
cpp 复制代码
// 1. 终止程序的函数​
[[noreturn]] void fatal_error(const std::string &message) {
    std::cerr << "Fatal error: " << message << std::endl;
    std::exit(EXIT_FAILURE);
    // 不需要 return 语句​
}
// 2. 总是抛出异常的函数​
[[noreturn]] void throw_runtime_error(const char *message) {
    throw std::runtime_error(message);
    // 不会执行到这里​
}
// 3. 无限循环的函数​
[[noreturn]] void event_loop() {
    while (true) {
        // 处理事件...​
    }
    // 不会执行到这里​
}
  • [[deprecated]](C++14),它用于标记某个实体(如函数、类、变量、类型等)已被弃用,不建议再使用。当编译器遇到使用被标记为 deprecated 属性的实体时,会发出编译警告
  • [[fallthrough]] (C++17), 用于在 switch 语句中显式地表明从一个 case 标签"贯穿"到下一个 case 标签是有意为之的,以避免编译器发出警告。
  • [[nodiscard]] (C++17),用于标记一个函数的返回值非常重要,调用者不应该忽略它。如果调用者没有使用返回值,编译器会发出警告。C++20 允许添加原因信息:[[nodiscard("xxx")]]
  • [[maybe_unused]] (C++17),表示有些定义的实体未使用是可以理解的,用于抑制编译器对未使用实体发出的"未使用变量/参数"警告
  • [[no_unique_address]](C++20)用于优化类的内存布局。它告诉编译器:被修饰的非静态数据空成员可能不需要在对象中拥有独立的地址空间,空成员就是无非静态成员变量的类对象。

C++定义的标准属性:

五、求值顺序

在 C++17 之前,很多表达式的求值顺序是未指定的,这意味着编译器可以自由选择求值顺序,这导致了不确定性和潜在的 bug。C++17 的求值顺序规则,提高了代码的可预测性和安全性,消除了许多历史遗留的未定义行为问题。

但最好还是不要依赖未指定的求值顺序,即使 C++17 修复了一些问题,最好还是写出不依赖特定求值顺序的代码,否则代码可读性维护性都会变差,建议将复杂的表达式分解成多个简单的语句。

表达式类型 C++17 前 C++17 后
a = b 未指定 先求值 b,再求值 a
a += b, a -= b 等 未指定 先求值 b,再求值 a
a << b, a >> b 未指定 从左到右求值
a[b] 未指定 先求值 a,再求值 b
a->b 未指定 先求值 a,再求值 b
a.b 未指定 先求值 a,再求值 b
f(a, b, c) 未指定 参数求值顺序仍未指定,但都在函数调用前完成
new Type(a, b) 未指定 先求值所有参数,再分配内存

本篇完,感谢阅读。

相关推荐
【心态好不摆烂】44 分钟前
数据结构基础精讲3
数据结构·算法
总要学点什么1 小时前
第108篇:三维特效-图片轨迹线效果——把贴图滚成一串箭头,想用它的本色反而整条消失
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
武子康1 小时前
ComfyUI 加载了动作 LoRA,H3-World 的时间绑定就接好了吗?
人工智能·机器学习·claude
漂流瓶jz2 小时前
UVA-1613 K度图的着色 题解答案代码 算法竞赛入门经典第二版
算法·深度优先·图论·贪心·aoapc·算法竞赛入门经典·uva
Pniubi3 小时前
动态规划入门 ( - 力扣509斐波那契数)
算法·动态规划
吴长建先生重名了4 小时前
GitHub Actions部署报表系统踩坑记:幂等性与限
算法
贝壳6904 小时前
一个2026年2月才上传的库,凭什么敢碰2009年就有人写烂的题目
算法
贝壳6905 小时前
同一道三亿位选型题,三个AI给了我三个答案,我把它们拉进同一台机器复跑
算法
爱和冰阔落5 小时前
【Linux网络】TCP 半包粘包怎么解决?从长度报头、JSON 拆包到网络计算器完整实战
linux·运维·网络·tcp/ip