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. 代码标注
  • 第二部分 - 标准库的变化
  • 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]]强制执行代码契约
    • 18. 使用[[nodiscard]]强制执行代码契约
      • 引言
      • 适用场景
      • 错误处理
      • 工厂函数/句柄
      • 返回非平凡类型时
      • 无副作用的代码
      • 处处使用?
      • 如何忽略[[nodiscard]]
      • C++17之前的情况
      • 总结
  • 19. 用if constexpr替换enable_if——带可变参数的工厂函数
  • 20. 如何实现CSV读取器的并行化
目录

18. 使用[[nodiscard]]强制执行代码契约

# 18. 使用[[nodiscard]]强制执行代码契约

C++17引入了一些新的标准属性。通过使用这些额外的注解,不仅能让其他开发者更易读懂你的代码,编译器也能利用这些信息。例如,它可能会针对潜在错误生成更多警告。反之,它也可能因为识别到正确意图(比如使用[[maybe_unused]]时)而避免生成警告。

在本章中,你将了解如何使用[[nodiscard]]属性,为代码提供更高的安全性。

# 引言

在“属性”章节中提到过[[nodiscard]]属性,这里通过一个简单示例回顾其特性。

该属性用于标记函数的返回值:

[[nodiscard]] int Compute();
1

当调用这样的函数却不赋值返回结果时:

void Foo() {
    Compute();
}
1
2
3

你应该会得到如下(或类似)的警告:

warning: ignoring return value of 'int Compute()', declared with attribute nodiscard
1

我们还可以更进一步,不仅标记返回值,还能标记整个类型:

[[nodiscard]]
struct ImportantType {};

ImportantType CalcSuperImportant();
1
2
3
4

每当调用任何返回ImportantType的函数时,都会收到警告。

换句话说,可以通过[[nodiscard]]强化函数的代码契约,让调用者不会忽略返回值。有时忽略返回值可能会引发错误,因此使用[[nodiscard]]能提高代码的安全性。

编译器会生成警告,但在构建代码时,通常最好启用“将警告视为错误”的设置。在MSVC中是/WX,在GCC中是-Werror。错误会停止编译过程,因此程序员需要采取措施修复代码。

# 适用场景

属性是一种标准化的代码注释方式。它们是可选的,但能帮助编译器优化代码、检测潜在错误,或者清晰地表达程序员的意图。

[[nodiscard]]在以下几个场景可能会很有用:

# 错误处理

[[nodiscard]]一个关键的用例是错误代码。

你有多少次忘记检查函数返回的错误代码了呢?(如果你不依赖异常处理,这一点至关重要)

下面是一些代码示例:

enum class [[nodiscard]] ErrorCode {
    OK,
    Fatal,
    System,
    FileIssue
};
1
2
3
4
5
6

假设有几个函数:

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

现在,每次调用这些函数时,都“被迫”检查返回值。

你可能经常看到这样的代码:开发者只检查了部分函数调用的返回值,而其他函数调用则未检查。这会导致不一致性,并可能引发严重的运行时错误。

你可能认为自己的方法运行正常(因为调用的M个函数中有N个返回了OK),但某些地方仍然出现故障。使用调试器检查时,你会发现Y函数返回了FAIL,而你却没有检查它。

应该使用[[nodiscard]]标记错误类型,还是只标记某些关键函数呢?

对于在整个应用程序中都可见的错误代码,标记错误类型可能是正确的做法。当然,如果函数只返回bool类型,那么只能标记函数,而不能标记类型(或者可以创建一个类型定义/别名,然后用[[nodiscard]]标记它)。

# 工厂函数/句柄

[[nodiscard]]发挥作用的另一类重要函数是“工厂函数”。每次调用“make/create/build”这类函数时,都不希望忽略返回值。这可能是显而易见的,但在进行一些重构时,仍有可能忘记或者注释掉对返回值的处理。

[[nodiscard]] Foo MakeFoo();
1

# 返回非平凡类型时

像下面这样的代码呢?

std::vector<std::string> GenerateNames();
1

返回的类型看起来比较复杂,通常意味着之后需要使用它。另一方面,就特定问题的语义而言,即使是int类型也可能很重要。

# 无副作用的代码

上一节的代码:

std::vector<std::string> GenerateNames(); // 无副作用...
1

这也是一个无副作用函数的示例,调用过程中不会改变全局状态。在这种情况下,需要对返回值进行处理。否则,该函数调用可能会从代码中被移除或优化掉。

# 处处使用?

有一篇论文P0600R0 - 《库中的[[nodiscard]]》 (opens new window) 可以作为参考指南。该提案未被纳入C++17,但在C++20中获得通过。它建议了一些应该应用该属性的场景:

对于现有API:
- 不使用返回值总是一个 “严重错误”(例如,总是会导致资源泄漏)
- 不使用返回值会引发问题,而且很容易发生(不容易察觉有问题)

对于新API(尚未纳入C++标准):
- 不使用返回值通常是一个错误。

以下是一些应该添加新属性的示例:

  • malloc()/new/allocate:这些是开销较大的调用,通常不使用返回值会导致资源泄漏。
  • std::async():不使用返回值会使调用变为同步,这可能很难察觉。

另一方面,像stop()这样的函数则存在争议,因为“不太有用,但也没有危险,而且这样的代码可能存在”。

或许最好不要在代码的所有地方都添加[[nodiscard]],而是关注关键部分。错误代码和工厂函数可能是不错的切入点。

# 如何忽略[[nodiscard]]

在极少数情况下,你可能希望抑制“未使用变量”警告。为此,可以使用C++17中的另一个属性[[maybe_unused]]:

[[nodiscard]] int Compute() { return 42; }
[[maybe_unused]] auto t = Compute();
1
2

此外,如“属性”章节所述,可以将函数调用强制转换为void,这样编译器会认为你“使用”了该值:

[[nodiscard]] int Compute();
static_cast<void>(Compute()); // 已使用
1
2

另一个不错的替代方法是编写一个单独的函数,包装结果并假装使用它 :

template <class T> inline void discard_on_purpose(T&&) {}
discard_on_purpose(Compute());
1
2

在使用避免[[nodiscard]]警告的技巧时要谨慎。最好遵循该属性的规则,而不是人为地规避警告。

# C++17之前的情况

大多数标准化的[[attrib]]属性都源自编译器扩展,[[nodiscard]]也是如此。

例如,在GCC/Clang中有attribute ((warn_unused_result));MSVC提供了_Check_return_,可查看MSDN:《注释函数行为 (opens new window)》 了解详情。

# 总结

总之,[[nodiscard]]是对所有重要代码(如公共API、安全关键系统等)的出色补充。添加此属性至少可以强化代码契约,并且编译器能帮助你在编译时检测错误,而不是在运行时才发现问题。

上次更新: 2025/04/01, 13:21:34
17. 使用std::optional和std::variant进行重构
19. 用if constexpr替换enable_if——带可变参数的工厂函数

← 17. 使用std::optional和std::variant进行重构 19. 用if constexpr替换enable_if——带可变参数的工厂函数→

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