CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
  • C++17 详解 说明
  • 第一部分——语言特性
  • 1. 快速入门
  • 2. 移除或修正的语言特性
  • 3. 语言澄清(Language Clarification)
  • 4. 通用语言特性
  • 5. 模板(Templates)
  • 6. 代码标注
    • 6. 代码标注
      • 为什么我们需要代码标注?
      • C++11之前
      • GCC特定代码标注
      • MSVC特定属性
      • Clang特定属性
      • C++11和C++14中的代码标注
      • C++17新增内容
      • 额外信息
      • [[fallthrough]]标注
      • [[maybe_unused]]标注
      • [[nodiscard]]标注
      • 命名空间和枚举器的标注
      • 额外信息
      • 忽略未知标注
      • 额外信息
      • 无需重复使用标注命名空间
      • 额外信息
      • 章节总结
      • 编译器支持
  • 第二部分 - 标准库的变化
  • 7. std::optional
  • 8. std::variant
  • 9. std::any
  • 10. std::string_view
  • 11. 字符串转换
  • 12. 搜索器与字符串匹配
  • 13. 文件系统
  • 14. 并行STL算法
  • 15. 标准库中的其他变化
  • 16. 移除和弃用的库特性
  • 第三部分 - 更多示例和用例
  • 17. 使用std::optional和std::variant进行重构
  • 18. 使用[[nodiscard]]强制执行代码契约
  • 19. 用if constexpr替换enable_if——带可变参数的工厂函数
  • 20. 如何实现CSV读取器的并行化
目录

6. 代码标注

# 6. 代码标注

代码标注可能并非C++广为人知的特性。然而,它对于向编译器传达额外信息以及帮助其他程序员理解代码很有帮助。自C++11起,有了一种标准的指定代码标注的方式。

在C++17中,我们又新增了更多实用的代码标注。在本章中,你将了解到:

  • C++中的代码标注是什么;
  • 特定厂商的代码标注与标准代码标注的区别;
  • 在哪些情况下代码标注很有用;
  • C++11和C++14中的代码标注;
  • C++17中的新增代码标注。

# 为什么我们需要代码标注?

你在代码中使用过declspec、attribute或#pragma指令吗?例如:

// 设置对齐方式
struct S { short f[3]; } attribute ((aligned (8)));
// 该函数不会返回
void fatal () attribute ((noreturn)); 
// 在MSVC中用于DLL导入/导出
#if COMPILING_DLL
#define DLLEXPORT    declspec(dllexport) 
#else
#define DLLEXPORT    declspec(dllimport) 
#endif
1
2
3
4
5
6
7
8
9
10

这些是现有的特定于编译器的代码标注形式。那么,代码标注究竟是什么呢?

代码标注是一种额外的信息,编译器可利用它生成代码。它可用于优化或特定的代码生成(比如DLL相关操作、OpenMP等)。此外,代码标注能让你编写更具表现力的语法,并帮助其他开发者理解代码。

与诸如C#之类的其他语言不同,在C++中,编译器固定了元信息系统,你无法添加用户自定义代码标注。而在C#中,你可以从System.Attribute派生。

现代C++代码标注的优势是什么呢?

自C++11起,越来越多的代码标注实现了标准化,可在不同编译器上通用。我们正从特定于编译器的注释转向标准形式。你无需学习各种标注语法,而是能够编写通用且行为一致的代码。

在下一节中,你将了解在C++11之前代码标注是如何使用的。

# C++11之前

在C++98/03时代,每个编译器都引入了各自的代码标注集,且通常使用不同的关键字。

你经常会看到代码中到处散布着#pragma、declspec、attribute。

以下是GCC/Clang和MSVC的常见语法列表:

# GCC特定代码标注

GCC使用attribute ((attr_name))形式的注释。例如:

int square (int ) attribute ((pure)); // 纯函数
1

文档:

  • 《使用GNU编译器集合(GCC):代码标注语法 (opens new window)》
  • 《使用GNU编译器集合(GCC):常见函数标注 (opens new window)》

# MSVC特定属性

微软主要使用declspec关键字作为各种编译器扩展的语法。相关文档见:__declspec (opens new window)(微软文档) 。

declspec(deprecated) void LegacyCode() { }
1

# Clang特定属性

Clang易于定制,能支持不同类型的代码标注,更多内容可查看其文档。大多数GCC的代码标注在Clang中也能使用。

相关文档见:《Clang中的属性 —— Clang文档 (opens new window)》。

# C++11和C++14中的代码标注

C++11朝着减少使用特定厂商语法的方向迈出了一大步。通过引入标准格式,我们可以将许多特定于编译器的属性整合到通用集合中。

C++11为在代码中指定代码标注提供了更简洁的格式。基本语法是[[attr]]或[[namespace::attr]]。

你几乎可以在任何内容上使用[[attr]],如类型、函数、枚举等等。例如:

[[attrib_name]] void foo() { }      // 用于函数
struct [[deprecated]] OldStruct { } // 用于结构体
1
2

在C++11中,有以下属性:

  • [[noreturn]]:它告诉编译器控制流不会返回给调用者。例如:
    • [[noreturn]] void terminate() noexcept;
    • 像std::abort或std::exit这样的函数也用这个标注标记。
  • [[carries_dependency]]:表明在release-consume的std::memory_order中的依赖链在函数内外传播,这能让编译器跳过不必要的内存屏障指令。主要用于优化多线程代码以及在使用不同内存模型时。

C++14添加了:

  • [[deprecated]]和[[deprecated("reason")]]:标记了这个标注的代码会被编译器报告。你可以设置弃用原因。

[[deprecated]]的示例:

[[deprecated("use AwesomeFunc instead")]] void GoodFunc() { }
// 在某处调用:
GoodFunc();
1
2
3

GCC会报告以下警告:

warning: 'void GoodFunc()' is deprecated: use AwesomeFunc instead [-Wdeprecated-declarations]
1

你已经对旧的方法以及C++11/14中的新方式有了一些了解…… 那么C++17又有什么新变化呢?

# C++17新增内容

C++17引入了三个新的标准代码标注:

  • [[fallthrough]]
  • [[nodiscard]]
  • [[maybe_unused]]
# 额外信息

这些新代码标注在P0188 (opens new window)和P0068 (opens new window)(论证)中被指定。

此外,还有三个支持特性:

  • 命名空间和枚举器的代码标注
  • 忽略未知代码标注
  • 无需重复使用代码标注命名空间

让我们先来了解一下这些新代码标注。

# [[fallthrough]]标注

该标注表明switch语句中的穿透(fall-through)是有意为之,不应为此发出警告。

switch (c) {
case 'a':
    f(); // 警告!穿透可能是程序员的错误
case 'b':
    g();
    [[fallthrough]]; // 警告被抑制,穿透是允许的
case 'c':
    h();
}
1
2
3
4
5
6
7
8
9

有了这个标注,编译器可以理解程序员的意图。而且它比使用注释更具可读性。

# [[maybe_unused]]标注

该属性用于抑制编译器关于未使用实体的警告:

static void impl1() { ... } // 当函数未被调用时,编译器可能会发出警告
[[maybe_unused]] static void impl2() { ... } // 警告被抑制

void foo() {
    int x = 42; // 如果之后x未被使用,编译器可能会发出警告
    [[maybe_unused]] int y = 42; // 对y的警告被抑制
}
1
2
3
4
5
6
7

当某些变量和函数仅在调试路径中使用时,这种特性很有帮助。例如在assert()宏中:

void doSomething(std::string_view a, std::string_view b) {
    assert(a.size() < b.size());
}
1
2
3

如果之后a或b在这个函数中未被使用,那么编译器会在发布版本的构建中生成警告。使用[[maybe_unused]]标记给定的参数可以解决这个警告。

# [[nodiscard]]标注

[[nodiscard]]可以应用于函数或类型声明,以标记返回值的重要性:

[[nodiscard]] int Compute();

void Test() {
    Compute(); // 警告!带有nodiscard标注的函数返回值被丢弃
}
1
2
3
4
5

如果你忘记将结果赋值给变量,编译器应该发出警告。

这意味着你可以强制用户处理错误。例如,如果你忘记使用new或std::async()的返回值会发生什么呢?

此外,该标注还可以应用于类型。一个用例可能是错误码:

enum class [[nodiscard]] ErrorCode {
    OK,
    Fatal,
    System,
    FileIssue
};

ErrorCode OpenFile(std::string_view fileName);
ErrorCode SendEmail(std::string_view sendto,
                    std::string_view text);
ErrorCode SystemCall(std::string_view text);
1
2
3
4
5
6
7
8
9
10
11

现在,每次调用这些函数时,你都“被迫”检查返回值。对于重要的函数,检查返回码可能至关重要,使用[[nodiscard]]可以帮你避免一些错误。

你可能还会问,“不使用”返回值是什么意思呢?

在标准中,它被定义为“丢弃值表达式 (opens new window)” 。这意味着你调用一个函数仅仅是为了它的副作用。换句话说,周围没有if语句或赋值表达式。在这种情况下,当一个类型被标记为[[nodiscard]]时,编译器会被鼓励报告一个警告。

然而,要抑制这个警告,你可以显式地将返回值转换为void,或者使用[[maybe_unused]]:

[[nodiscard]] int Compute();

void Test() {
    static_cast<void>(Compute()); // 没问题...
    [[maybe_unused]] auto ret = Compute();
}
1
2
3
4
5
6

此外,在C++20中,标准库会在一些地方使用[[nodiscard]],比如:operator new、std::async()、std::allocate()、std::launder()和std::empty()。

这个特性已经通过P0600 (opens new window) 合并到C++20中。 C++20的第二个新增内容是[[nodiscard("reason")]],详见P1301 (opens new window) 。这允许你指定不使用返回值可能会产生问题的原因,例如,某些资源泄漏。

# 命名空间和枚举器的标注

C++11中引入属性的目的是能够将它们应用到所有合理的地方,如类、函数、变量、类型定义、模板、枚举等等。但在规范中有一个问题,当属性应用于命名空间或枚举器时会受到限制。

C++17修复了这个问题。现在我们可以这样写:

namespace [[deprecated("use BetterUtils")]] GoodUtils {
    void DoStuff() { }
}

namespace BetterUtils {
    void DoStuff() { }
}
// 使用:
GoodUtils::DoStuff();
1
2
3
4
5
6
7
8
9

Clang会报告:

warning: 'GoodUtils' is deprecated: use BetterUtils [-Wdeprecated-declarations]
1

另一个例子是在枚举器上使用deprecated标注:

enum class ColorModes {
    RGB [[deprecated("use RGB8")]],
    RGBA [[deprecated("use RGBA8")]],
    RGB8,
    RGBA8
};
// 使用:
auto colMode = ColorModes::RGBA;
1
2
3
4
5
6
7
8

在GCC下,我们会得到:

warning: 'RGBA' is deprecated: use RGBA8 [-Wdeprecated-declarations]
1
# 额外信息

该变更在N4266 (opens new window)(措辞)和N4196 (opens new window)(论证)中被描述。

# 忽略未知标注

这个特性主要是为了明确规范。

在C++17之前,如果你尝试使用一些特定于某个编译器的标注,在另一个不支持该标注的编译器中编译时,甚至可能会出错。现在,编译器会忽略标注规范,并且不会报告任何内容(或者仅给出一个警告)。这在标准中之前没有提及,需要进行明确。

// 不支持MyCompilerSpecificNamespace的编译器将忽略这个属性
[[MyCompilerSpecificNamespace::do_special_thing]]
void foo();
1
2
3

例如在GCC 7.1中会有一个警告:

warning: 'MyCompilerSpecificNamespace::do_special_thing'
scoped attribute directive ignored [-Wattributes]
void foo();
1
2
3
# 额外信息

该变更在P0283R2 (opens new window)(措辞)和P0283R1 (opens new window)(论证)中被描述。

# 无需重复使用标注命名空间

这个特性简化了需要使用多个标注的情况,比如:

void f() {
    [[rpr::kernel, rpr::target(cpu,gpu)]] // 有重复
    doTask(); 
}
1
2
3
4

提议的修改:

void f() {
    [[using rpr: kernel, target(cpu,gpu)]]
    doTask(); 
}
1
2
3
4

在构建将这种带注释的代码自动转换为不同编程模型的工具时,这种简化可能会有所帮助。

# 额外信息

该变更在P0028R4 (opens new window) 中被描述。

# 章节总结

C++17中可用的标注:

属性 描述
[[noreturn]] 函数不会返回给调用者
[[carries_dependency]] 关于依赖链的额外信息
[[deprecated]] 实体已被弃用
[[deprecated("reason")]] 提供关于弃用的额外信息
[[fallthrough]] 表明switch语句中的穿透是有意为之
[[nodiscard]] 如果返回值被丢弃,会生成一个警告
[[maybe_unused]] 代码中某个实体可能未被使用

每个编译器厂商都可以指定自己的标注和注释语法。在现代C++中,ISO委员会试图提取通用部分并将其标准化为[[attributes]]。

Bjarne Stroustrup的C++11常见问题解答 (opens new window) 中有一段关于建议用法的相关引用: “人们有理由担心属性会被用于创建语言方言。建议仅将属性用于控制那些不影响程序含义,但可能有助于检测错误(例如[[noreturn]])或帮助优化器(例如[[carries_dependency]])的方面。”

# 编译器支持

特性 GCC Clang MSVC
[[fallthrough]] 7.0 3.9 15.0
[[nodiscard]] 7.0 3.9 15.3
[[maybe_unused]] 7.0 3.9 15.3
命名空间和枚举器的标注 4.9/6¹⁶ 3.4 14.0
忽略未知属性 所有版本 3.9 14.0
无需重复使用标注命名空间 7.0 3.9 15.3

上述所有编译器也都支持C++11/14的标注。

批注 ¹⁶ GCC 4.9(命名空间)/ GCC 6(枚举)

上次更新: 2025/04/01, 13:21:34
5. 模板(Templates)
第二部分 - 标准库的变化

← 5. 模板(Templates) 第二部分 - 标准库的变化→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
第四章 Lambda函数
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式