
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 采用流水线技术,预测和执行后续的指令。当遇到条件分支(如
if或switch)时,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 {}; 的对象)都必须有一个唯一的地址
这意味着一个类的 struct 或 class(包含多个空类型的成员时),每个成员都会占用至少 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 函数返回 -1 或 string::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 就是一个接受返回值的,封装返回值的一个类,也就是一个接收器,正因为是被封装了,所以对于返回值的处理就会有更多自定义的效果与可能!