C++ 17 详细特性解析(3)

1. 🌟 属性是什么?

属性是为代码实体(变量、函数、类等)添加额外信息的一种机制,这些信息可以帮助编译器进行优化、生成警告或检查错误。在 C++11 之前,各编译器使用自己的语法和工具。

cpp 复制代码
// GCC 扩展语法
__attribute__((deprecated)) void f() {}

// MSVC 扩展语法
__declspec(deprecated) void g() {}

// C++11 统一标准属性语法
[[noreturn]] void f() {}
[[deprecated]] void g() {}  // 注意:deprecated是C++14加入的

C++11 将属性语法标准化为 [[...]],但只提供了两个标准属性。C++14/C++17/C++20 中属性逐步丰富化。🔗 参考:https://en.cppreference.com/w/cpp/language/attribute.html


2. ✨ 常用标准属性详解

👉 [[noreturn]] (C++11)

  • 指示函数不会返回到它的调用点,主要用于优化和错误检测
  • 明确告知编译器函数不会返回,编译器可以优化代码(如跳过某些无效的后续代码执行)
  • 标记为 noreturn 的函数返回时,编译器会触发警告
cpp 复制代码
#include <cstddef>
#include <iostream>
#include <stdexcept>

// 终止程序的函数
[[noreturn]] void fatal_error(const std::string& message) {
    std::cerr << "FATAL ERROR: " << message << std::endl;
    std::abort();  // 不需要return语句
}

// 总是抛出异常的函数
[[noreturn]] void throw_runtime_error(const char* message) {
    throw std::runtime_error(message);
    // 不会执行到这里
}

// 无限循环的函数
[[noreturn]] void event_loop() {
    while (true) {
        // 处理事件
    }
    // 不会执行到这里
}

// 编译器优化示例
int main() {
    fatal_error("Something went wrong!");
    // 下面的代码不会被执行,编译器会优化掉
    std::cout << "This line will never be reached." << std::endl;
    return 0;
}

👉 [[deprecated]] (C++14)

  • 用于标记某个实体(如函数、类、变量、类型等)已被弃用,不建议再使用
  • 当编译器遇到使用被标记为 deprecated 的实体时,会发出编译警告
cpp 复制代码
#include <iostream>

// 标记函数为弃用
[[deprecated]]
void old_function();

// 可以提供一条自定义的警告信息
[[deprecated("Use the new_function() instead, which is safer and faster.")]]
void legacy_function();

// 也可以标记类型别名、变量等
[[deprecated]]
typedef char Cstring;

struct [[deprecated]] OldStruct {};

[[deprecated]]
int obsolete_variable;

[[deprecated("This function is prone to errors. Use calculate() instead.")]]
int compute() {
    return 42;
}

// deprecated也可以用于类型、变量、枚举等
// v1.0
[[deprecated("这是v1.0版本,请使用v2.0改用connect(url, options)")]]
void connect(std::string url) {}

// v2.0
void connect(std::string url, int options) {}

int main() {
    int x = compute();  // 编译时会产生警告或错误
    std::cout << x << std::endl;

    Cstring ptr = nullptr;

    connect("http://example.com");  // 会触发警告

    return 0;
}

🔗 参考:https://en.cppreference.com/w/cpp/language/attributes.html


👉 C++17 新增重要属性

C++17 对属性系统进行了更重大的扩展,引入了三个非常重要的新属性,极大扩展了属性的应用范围。

[[fallthrough]] (C++17)

  • 用于在 switch 语句中显式表明从一个 case 标签 "贯穿" 到下一个 case 标签是有意为之的,以避免编译器发出警告(本来正常是需要进行 break 的!)

[[nodiscard]] (C++17)

  • 用于标记一个函数的返回值非常重要,调用者不应该忽略它
  • 如果调用者没有使用返回值,编译器会发出警告
  • C++20 允许为 nodiscard 添加自定义提示信息

[[maybe_unused]] (C++17)

  • 用于抑制编译器对未使用的变量 / 参数发出的 "未使用变量 / 参数" 警告
  • 之前对未使用的变量都是:(void)变量
cpp 复制代码
#include <iostream>

void check_value(int value) {
    switch (value) {
        case 1:
            std::cout << "Case 1" << std::endl;
            [[fallthrough]];  // 故意贯穿,明确告知编译器
        case 2:
            std::cout << "Case 2" << std::endl;
            break;
        default:
            std::cout << "Default case" << std::endl;
    }
}

[[nodiscard]] int compute_something_important() {
    return 42;
}

int main() {
    check_value(1);

    compute_something_important();  // 编译时会产生警告,因为返回值被忽略了
    int result = compute_something_important();
    std::cout << "Result: " << result << std::endl;

    return 0;
}

👉 C++20 新增属性

[[likely]][[unlikely]] (C++20)

  • 这两个属性是用于向编译器提供分支预测的提示,帮助编译器优化代码
  • 现代 CPU 采用流水线技术,预测和执行后续的指令。当遇到条件分支(如 ifswitch)时,CPU 必须猜测哪条路径更可能被执行
  • 如果猜测错误(分支预测错误),就需要清空流水线,重新加载正确的指令,这会带来显著的性能惩罚
  • [[likely]] 向编译器指示,它所在的代码路径(例如 if 语句的 true 分支,或 switch 语句的某个 case)在程序的执行过程中是更可能被执行的
  • [[unlikely]] 则相反,指示代码路径不太可能被执行
  • 只有当你有充分的理由证明某个分支确实在极端概率下发生时,才使用这些属性,盲目使用可能会误导编译器,适得其反
cpp 复制代码
int process(int value) {
    if (value > 0) [[likely]] {
        return value;
    } else [[unlikely]] {
        // 错误处理,很少执行
        return -value;
    }
}

void func() {
    int status = 0;
    std::cin >> status;

    switch (status) {
        case 0: [[likely]]
            break;
        case 1: [[unlikely]]
            // 处理错误
            break;
        default: [[unlikely]]
            handle_unknown();
            break;
    }
}

int main() {
    func();
    func();
    return 0;
}

[[no_unique_address]] (C++20)

用于优化类的内存布局。它告诉编译器,被修饰的非静态数据成员可能不需要在对象中拥有独立的地址空间

空成员是指无静态数据成员的类对象。在 C++ 中,任何对象(即使是空类 class Empty {}; 的对象)都必须有一个唯一的地址

这意味着一个类的 structclass(包含多个空类型的成员时),每个成员都会占用至少 1 字节,再加上内存对齐的填充字节,会导致大量内存浪费

空基类优化(EBO - Empty Base Optimization, C++ 标准允许编译器对空基类进行优化。如果一个类继承自空基类,编译器可以不为其子对象分配任何空间,使其大小为零)

[[no_unique_address]] 是 EBO 的一个推广,它可以应用于非静态数据成员中,提示编译器成员可以与其他成员共享地址空间

要注意如果是相同类型的多个成员,这个优化可能会失效

cpp 复制代码
struct Empty {};

struct Foo {
    [[no_unique_address]] Empty e1;
    [[no_unique_address]] Empty e2;
    int x;
};

struct Bar {
    Empty e1;
    Empty e2;
    int x;
};

struct X : Empty {
    int x;
};

struct Y : Empty, Empty {
    int y;
};

template <typename Allocator = std::allocator<int>>
struct MyVector {
    int data;
    size_t size;
    size_t capacity;
    [[no_unique_address]] Allocator alloc;
};

int main() {
    std::cout << sizeof(Foo) << std::endl;
    std::cout << sizeof(Bar) << std::endl;
    std::cout << sizeof(X) << std::endl;
    std::cout << sizeof(Y) << std::endl;

    return 0;
}

3. 🤔 新的求值顺序规则

这个其实我感觉没啥用,也别用,在代码中写容易误导被人,会被别人 out !

https://en.cppreference.com/w/cpp/language/eval_order.html

C++17 之前 ,很多表达式的求值顺序是**未指定(unspecified)**的,这意味着编译器可以自由选择求值顺序,导致了不确定性和潜在的 bug。

C++17 的求值顺序规则 大大提高了代码的可预测性和安全性,消除了许多历史遗留的未定义行为问题。(所以就是解决代码出现的二义性BUG,让本来就是不好的代码变得可预测,反正好好写就没啥问题)

💡 建议 :还是不要依赖未指定的求值顺序。即使 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) 未指定 先求值所有参数,再分配内存
cpp 复制代码
#include <iostream>

void process(int a, int b) {
    std::cout << "a = " << a << ", b = " << b << std::endl;
}

int main() {
    int x = 0;

    // C++17 前:未指定行为!
    process(x++, x++); // 可能输出 (0, 1) 或 (1, 0)

    int i = 0;
    int j = 0;

    // C++17 前:未定义行为!
    // C++17 后:明确先求值右边(i++),再求值左边(i)
    i = i++; // 现在明确:i = 1

    std::map<int, int> m = { {1, 10}, {2, 20} };
    auto it = m.begin();

    // C++17 前:未定义行为!it++ 和 it->second 的求值顺序不确定
    // C++17 后:明确先求值 it->second,再求值 it++

    int value = it++->second; // 安全:value = 10,it 指向第二个元素
    std::cout << value << std::endl;

    return 0;
}

4. 🎁 std::optional


🔍 什么是 std::optional

  • 官方定义std::optional<T> 是一个类模板,它表示一个可能包含一个类型为 T 的值,也可能不包含任何值(即 "空" 状态)。
  • 核心价值 :它是一种类型安全 的方式,用来替代诸如 "返回特殊值(如 -1, nullptr, EOF 等)" 或 "使用输出参数" 等传统模式。
  • 重要意义std::optional 是 C++17 中一个简单却极其有用的工具,它极大地提高了代码的可读性和安全性。

🔗 参考:https://en.cppreference.com/w/cpp/header/optional.html


🤔 为什么需要 std::optional

在没有 std::optional 之前,我们通常用以下不太优雅的方式处理 "可能无返回值" 的情况:

返回特殊值 :例如 find 函数返回 -1string::npos,返回 nullptr 指针等。

  • ❌ 问题:这些特殊值没有类型安全保证,调用者很容易忘记检查,代码可读性差。

使用输出参数 :通过引用传递参数来存储结果,函数本身返回一个 bool 表示成功与否。

  • ❌ 问题:语法笨拙,不够直观。

抛出异常:并非所有 "无结果" 的情况都是异常,有时它只是一个正常的、可预期的分支。

  • ❌ 问题:使用异常来控制流程开销较大且不直观。

std::optional 完美解决了这些问题,它将值(或有或无)包装在一个类型中,强制调用者处理可能无值的情况


🛠️ 常见接口

特性 说明 代码示例
创建空 表示无值 std::optional<int> empty; auto empty = std::nullopt;
创建有值 包装一个值 std::optional<int> opt = 5; auto opt = std::make_optional(5);
检查 判断是否包含值 if (opt.has_value()) [...] if (opt) [...]
安全取值 有值返回值,无值抛异常 int x = opt.value();
安全取值 (带默认) 无值时返回默认值 int x = opt.value_or(0);
不安全取值 必须确保有值,否则 UB int x = *opt;
重置 使其变为空 opt.reset(); opt = std::nullopt;

💻 代码示例:基本使用
cpp 复制代码
#include <iostream>
#include <vector>
#include <map>
#include <optional>

// 结合文档,optional的基本使用
void test_example1()
{
    // 1、定义optional对象
    std::optional<int> maybeInt;              // 初始为空
    std::optional<std::string> maybeString = "Hello"; // 初始有值
    std::optional<double> empty = std::nullopt; // 显式设置为空

    // 2、检查是否有值
    if (maybeInt.has_value()) {
        std::cout << "has_value1" << std::endl;
    }

    // 或者更简洁的写法
    if (maybeString) {
        std::cout << "has_value2" << *maybeString << std::endl;
    }

    // 3、访问值
    // 安全访问 - 无值时抛出 std::bad_optional_access
    try {
        int value = maybeInt.value();
    }
    catch (const std::bad_optional_access& e) {
        std::cout << e.what() << std::endl;
    }

    maybeInt = 1;
    // 不安全但快速的访问 - 无值时行为未定义
    int value1 = *maybeInt;

    // 带默认值的访问
    int value2 = maybeInt.value_or(2); // 无值时返回2
    std::cout << value2 << std::endl;

    // 4、修改值
    maybeInt = 42;                // 赋新值
    maybeInt = std::nullopt;      // 设为空
    maybeInt.reset();             // 设为空
}

这段代码围绕 std::optional(C++17 引入的可选值类型)展开了完整的基础用法演示:

std::optional 作为 C++17 引入的可选值类型,其基础用法覆盖了完整的对象操作逻辑:在对象定义阶段,支持默认空值初始化、直接赋有效值、通过 std::nullopt 显式设为空三种方式;

值存在性检查可通过 has_value() 成员函数或隐式布尔转换两种简洁方式完成;

值访问则提供了多场景方案 ------value() 是安全访问方式(无值时抛出 std::bad_optional_access 异常),**解引用 *** 是快速访问方式(无值时行为未定义),value_or() 则支持带默认值访问(无值时返回指定默认值,避免行为未指定);

值的修改与清空也有三种操作形式,既可以直接赋值新值,也能通过 std::nullopt 或 reset() 方法将其设为空。

从核心特性来看,std::optional<T> 是封装 "可能有值、也可能无值" 的 T 类型对象的模板类,本质是对 "值 + 存在状态" 的封装 ,无需借助指针或魔法值表示 "无值";它具备值语义,可直接拷贝、赋值并存储在容器中,行为与普通类型一致且无需手动管理内存;同时实现了类型安全,能明确区分 "无值" 和 "有值",避免将 "无值标识" 误判为有效数据;在性能上它是零开销抽象,有值时仅比 T 多一个布尔值的内存开销,且编译器通常会优化该布尔值存储;异常安全层面,value() 访问的异常抛出机制便于捕获无值错误,value_or() 则能优雅处理无值场景。

std::optional 的核心作用体现在三个方面:

  • 替代魔法值,解决了用特殊值(如 -1)表示无结果的问题,函数可直接返回 std::optional 类型,有值返回有效值、无值返回空对象;
  • 替代空指针,相比裸指针(如 T*)更安全,不会出现空指针解引用,也无需手动释放内存;
  • 简化判空逻辑、提升代码可读性,在函数返回值、参数传递等场景中,std::optional 能清晰表达 "可选性",读者可直观理解值的可选属性,无需依赖注释说明,大幅降低代码维护成本。

💼 代码示例:实践中的使用场景
cpp 复制代码
// optional实践中的使用场景
void test_example2()
{
    // 场景1:容器查找 - 有结果返回值,无结果返回空
    std::map<std::string, int> indexMap = {{"张庄",1}, {"王村",2}, {"李家村",3}, {"王家坪",3}};

    auto findIndex = [&indexMap](const std::string& str)->std::optional<int>
    {
        auto it = indexMap.find(str);
        if (it != indexMap.end()) {
            return it->second; // 找到则返回有效值
        } else {
            return std::nullopt; // 没找到则返回空
        }
    };

    std::string x;
    std::cin >> x;
    std::optional<int> index = findIndex(x);
    if (index) { // 简洁判断是否有值
        std::cout << x << "对应的编号为: " << *index << std::endl;
    } else {
        std::cout << x << "是非法顶点" << std::endl;
    }

    // 场景2:容器访问越界 - 区分"空值"和"有效空字符串"
    std::vector<std::string> v = {"张庄", "李庄", ""};
    auto access = [&v](int i)->std::optional<std::string>
    {
        if (i < v.size()) {
            return v[i]; // 下标合法则返回对应值(包括空字符串"")
        } else {
            return std::nullopt; // 下标越界则返回空(关键:区分"越界无值"和"有效空字符串")
        }
    };
}

int main()
{
    test_example1();
    test_example2();
    return 0;
}

容器查找结果封装findIndex 函数封装了 map 的查找逻辑,找到目标则返回对应的 int 编号,没找到则返回 std::nullopt------ 相比用 -1 这类魔法值表示 "未找到",optional明确区分 "有效编号" 和 "未找到" ,避免魔法值和有效数据冲突(比如编号本身可能有 -1 的情况)。

容器越界访问处理access 函数处理 vector 下标访问,下标合法时返回对应值(包括空字符串 ""),下标越界时返回 std::nullopt------ 解决了 "有效空值" 和 "访问失败" 的区分问题(比如 v[i] 本身可能是 "",无法用返回值判断是否越界),相比抛异常 / 断言,optional 让调用方可以优雅地非异常处理越界场景,代码更灵活。

调用方简洁判空 :无论是查找结果还是访问结果,调用方都可以通过 if (index) 这种极简方式判断是否有有效值,逻辑清晰、可读性高,无需额外判断魔法值或捕获异常。

反正我觉得最直接的就是 optional 就是一个接受返回值的,封装返回值的一个类,也就是一个接收器,正因为是被封装了,所以对于返回值的处理就会有更多自定义的效果与可能!

相关推荐
2601_949480061 小时前
Flutter for OpenHarmony音乐播放器App实战11:创建歌单实现
开发语言·javascript·flutter
java1234_小锋2 小时前
高频面试题:Java中如何安全地停止线程?
java·开发语言
一晌小贪欢2 小时前
Python 操作 Excel 高阶技巧:用 openpyxl 玩转循环与 Decimal 精度控制
开发语言·python·excel·openpyxl·python办公·python读取excel
C+-C资深大佬2 小时前
C++多态
java·jvm·c++
Coder_preston2 小时前
JavaScript学习指南
开发语言·javascript·ecmascript
今儿敲了吗2 小时前
11| 子集
c++·笔记·算法
阿猿收手吧!2 小时前
【C++】无锁原子栈:CAS实现线程安全
开发语言·c++·安全
写代码的【黑咖啡】2 小时前
Python 中的自然语言处理工具:spaCy
开发语言·python·自然语言处理
沐知全栈开发2 小时前
WSDL 语法详解
开发语言