18. 使用[[nodiscard]]强制执行代码契约
# 18. 使用[[nodiscard]]强制执行代码契约
C++17引入了一些新的标准属性。通过使用这些额外的注解,不仅能让其他开发者更易读懂你的代码,编译器也能利用这些信息。例如,它可能会针对潜在错误生成更多警告。反之,它也可能因为识别到正确意图(比如使用[[maybe_unused]]
时)而避免生成警告。
在本章中,你将了解如何使用[[nodiscard]]
属性,为代码提供更高的安全性。
# 引言
在“属性”章节中提到过[[nodiscard]]
属性,这里通过一个简单示例回顾其特性。
该属性用于标记函数的返回值:
[[nodiscard]] int Compute();
当调用这样的函数却不赋值返回结果时:
void Foo() {
Compute();
}
2
3
你应该会得到如下(或类似)的警告:
warning: ignoring return value of 'int Compute()', declared with attribute nodiscard
我们还可以更进一步,不仅标记返回值,还能标记整个类型:
[[nodiscard]]
struct ImportantType {};
ImportantType CalcSuperImportant();
2
3
4
每当调用任何返回ImportantType
的函数时,都会收到警告。
换句话说,可以通过[[nodiscard]]
强化函数的代码契约,让调用者不会忽略返回值。有时忽略返回值可能会引发错误,因此使用[[nodiscard]]
能提高代码的安全性。
编译器会生成警告,但在构建代码时,通常最好启用“将警告视为错误”的设置。在MSVC中是/WX
,在GCC中是-Werror
。错误会停止编译过程,因此程序员需要采取措施修复代码。
# 适用场景
属性是一种标准化的代码注释方式。它们是可选的,但能帮助编译器优化代码、检测潜在错误,或者清晰地表达程序员的意图。
[[nodiscard]]
在以下几个场景可能会很有用:
# 错误处理
[[nodiscard]]
一个关键的用例是错误代码。
你有多少次忘记检查函数返回的错误代码了呢?(如果你不依赖异常处理,这一点至关重要)
下面是一些代码示例:
enum class [[nodiscard]] ErrorCode {
OK,
Fatal,
System,
FileIssue
};
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);
2
3
4
现在,每次调用这些函数时,都“被迫”检查返回值。
你可能经常看到这样的代码:开发者只检查了部分函数调用的返回值,而其他函数调用则未检查。这会导致不一致性,并可能引发严重的运行时错误。
你可能认为自己的方法运行正常(因为调用的M
个函数中有N
个返回了OK
),但某些地方仍然出现故障。使用调试器检查时,你会发现Y
函数返回了FAIL
,而你却没有检查它。
应该使用[[nodiscard]]
标记错误类型,还是只标记某些关键函数呢?
对于在整个应用程序中都可见的错误代码,标记错误类型可能是正确的做法。当然,如果函数只返回bool
类型,那么只能标记函数,而不能标记类型(或者可以创建一个类型定义/别名,然后用[[nodiscard]]
标记它)。
# 工厂函数/句柄
[[nodiscard]]
发挥作用的另一类重要函数是“工厂函数”。每次调用“make
/create
/build
”这类函数时,都不希望忽略返回值。这可能是显而易见的,但在进行一些重构时,仍有可能忘记或者注释掉对返回值的处理。
[[nodiscard]] Foo MakeFoo();
# 返回非平凡类型时
像下面这样的代码呢?
std::vector<std::string> GenerateNames();
返回的类型看起来比较复杂,通常意味着之后需要使用它。另一方面,就特定问题的语义而言,即使是int
类型也可能很重要。
# 无副作用的代码
上一节的代码:
std::vector<std::string> GenerateNames(); // 无副作用...
这也是一个无副作用函数的示例,调用过程中不会改变全局状态。在这种情况下,需要对返回值进行处理。否则,该函数调用可能会从代码中被移除或优化掉。
# 处处使用?
有一篇论文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();
2
此外,如“属性”章节所述,可以将函数调用强制转换为void
,这样编译器会认为你“使用”了该值:
[[nodiscard]] int Compute();
static_cast<void>(Compute()); // 已使用
2
另一个不错的替代方法是编写一个单独的函数,包装结果并假装使用它 :
template <class T> inline void discard_on_purpose(T&&) {}
discard_on_purpose(Compute());
2
在使用避免[[nodiscard]]
警告的技巧时要谨慎。最好遵循该属性的规则,而不是人为地规避警告。
# C++17之前的情况
大多数标准化的[[attrib]]
属性都源自编译器扩展,[[nodiscard]]
也是如此。
例如,在GCC/Clang中有attribute ((warn_unused_result))
;MSVC提供了_Check_return_
,可查看MSDN:《注释函数行为 (opens new window)》 了解详情。
# 总结
总之,[[nodiscard]]
是对所有重要代码(如公共API、安全关键系统等)的出色补充。添加此属性至少可以强化代码契约,并且编译器能帮助你在编译时检测错误,而不是在运行时才发现问题。