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

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>