[[nodiscard]]
属性,该属性允许程序员通过以下方式标记函数:如果返回的对象被调用方丢弃,则编译器将生成警告。可以将相同的属性添加到整个类类型。我已经在原始提案中了解了此功能的动机,并且我知道C ++ 20会将属性添加到标准函数就像
std::vector::empty
一样,其名称并未对返回值传达明确的含义。这是一个很酷且有用的功能。实际上,这似乎太有用了。我到处都读到有关
[[nodiscard]]
的信息,人们在讨论它,就好像您只是将其添加到选定的几个函数或类型中,而忽略了其余的函数或类型一样。但是,为什么不可丢弃的值应该是特例,尤其是在编写新代码时? 被放弃的返回值通常不是bug还是至少浪费资源?
C ++本身的设计原则之一就是编译器应该捕获尽可能多的错误吗? br />
如果是这样,那为什么不将自己的非传统代码中的
[[nodiscard]]
添加到几乎每个非void
函数和几乎每个类类型中呢?我已经尝试过可以在我自己的代码中执行此操作,并且效果很好,除了它非常冗长,以至于开始像Java。使编译器默认发出丢弃的返回值警告似乎更为自然,除非您标记了意图[*]的其他少数情况。
由于在这种情况下我零次讨论了这种可能性标准提案,博客条目,堆栈溢出问题或Internet上的其他任何地方,我一定会缺少一些东西。
为什么这样的机制在新的C ++代码中没有意义?冗长程度是不是几乎在所有地方都不使用
[[nodiscard]]
的唯一原因吗?在标准库实现中。#1 楼
在不需要与旧标准兼容的新代码中,请在合理的地方使用该属性。但是对于C ++,[[nodiscard]]
的默认设置不正确。您建议:使编译器默认警告丢弃的返回值似乎更加自然,除非您标记了意图的其他几种情况。
这会突然导致现有的正确代码发出很多警告。尽管从技术上来说,这样的更改可以被认为是向后兼容的,因为任何现有的代码仍然可以成功地进行编译,但实际上这将是语义上的巨大变化。
使用很大的代码库必然与全新语言的设计决策有所不同。如果这是一种新语言,则默认情况下发出警告是明智的。例如,Nim语言要求将不需要的值显式丢弃–这类似于使用强制转换
(void)(...)
包装C ++中的每个表达式语句。[[nodiscard]]
属性在两种情况下最有用:如果函数除了返回特定结果外没有其他效果,即为纯函数。如果未使用结果,则该调用肯定是无用的。另一方面,丢弃结果将是不正确的。
如果必须检查返回值,例如对于类似C的界面,该界面返回错误代码而不是抛出错误。这是主要的用例。对于惯用的C ++,这将非常罕见。
这两种情况留下了不纯函数的巨大空间,这些函数确实返回了一个值,在这种情况下,这样的警告会产生误导。例如:
考虑具有
.pop()
方法的队列数据类型,该方法删除一个元素并返回删除的元素的副本。这种方法通常很方便。但是,在某些情况下,我们只希望删除元素而不获取副本。那是完全合法的,警告将无济于事。另一种设计(例如std::vector
)可以划分这些职责,但还有其他折衷。请注意,在某些情况下,无论如何都必须进行复制,因此感谢RVO返回该副本是免费的。
考虑流畅的接口,其中每个操作都返回对象,以便可以执行进一步的操作。在C ++中,最常见的示例是流插入运算符
<<
。向每个[[nodiscard]]
重载添加一个<<
属性将非常麻烦。 请注意,您闪亮的新C ++ 17代码(可以在其中使用这些属性)可能仍会与针对较旧C ++标准的库一起编译。向后兼容性对于C / C ++生态系统至关重要。因此,将nodiscard设置为默认值将对典型的惯用例产生许多警告-如果不对库的源代码进行深远的更改,就无法修复这些警告。更改了语义,但每个C ++标准的功能都适用于每个编译单元范围,而不适用于每个文件范围。如果/当将来的某些C ++标准从头文件移开时,这样的更改将更加现实。
评论
我对此表示赞同,因为它包含了有用的见解。不过,目前还不确定是否真的能回答这个问题。诸如pop或operator <<之类的不纯函数,将由我虚构的[[maydiscard]]属性显式标记,因此不会生成警告。您是说这样的功能通常比我想像的更普遍,或者比我自己的代码库更普遍?您关于现有代码的第一段当然是对的,但仍然让我想知道是否还有其他人正在使用[[nodiscard]]来添加其所有新代码,如果不是,为什么呢?
–克里斯蒂安·哈克(Christian Hackl)
17/12/30在18:22
@ChristianHackl:在C ++的历史中曾经有一段时间,将值作为const返回是一种很好的做法。您不能说returns_an_int()= 0,那么为什么returns_a_str()=“”有效? C ++ 11表明这实际上是一个可怕的想法,因为现在所有这些代码都禁止移动,因为值是const。我认为从该课程中得出的正确结论是“这取决于您的呼叫者如何处理您的结果”。忽略结果是呼叫者可能想做的完全正确的事情,除非您知道这是错误的(非常高的标准),否则不应阻止它。
– GManNickG
17年12月30日在20:34
在empty()上使用nodiscard也是明智的,因为该名称非常含糊:它是谓词is_empty()还是mutator clear()?通常,您不会期望此类mutator返回任何内容,因此,有关返回值的警告可能会提醒程序员出现问题。使用更清晰的名称,此属性将不是必需的。
–阿蒙
17年12月30日在22:02
@ChristianHackl:正如其他人所说,我认为纯函数是何时使用此函数的一个很好的例子。我会更进一步,说应该有一个[[pure]]属性,它应该暗示[[nodiscard]]。这样,很清楚为什么不应该丢弃此结果。但是除了纯函数之外,我无法想到应该具有这种行为的另一类函数。而且大多数功能都不是纯函数,因此我认为nodiscard作为默认值不是正确的选择。
– GManNickG
17年12月30日在22:46
@GManNickG:尝试并未能调试忽略错误代码的代码后,我坚信默认情况下需要检查绝大多数错误代码。
–鸭鸭
17年12月31日在9:10
#2 楼
我不会在所有地方都出现[[nodiscard]]
的原因:(主要:)这会在标题中引入过多的噪音。我应该对其他人的代码做出强烈的推测性假设。您想丢弃我给您的返回值吗?好的,把自己弄出来。
(minor :)您将保证与C ++ 14不兼容
现在,如果将其设置为默认值,则将强制所有库开发人员强制执行所有操作。他们的用户不放弃返回值。那太可怕了。或者您强迫他们在许多功能中添加
[[maydiscard]]
,这也很可怕。#3 楼
例如:operator <<的返回值取决于绝对必要或绝对无用的调用。 (std :: cout << x << y,第一个是必需的,因为它返回流,第二个完全没有用)。现在与printf进行比较,在每个人中,每个人都丢弃返回的值以进行错误检查,但是操作符<<没有错误检查,因此它起初没有那么有用。因此,在这两种情况下,丢弃都只会造成破坏。评论
这回答了一个未被问到的问题,即“为什么不在任何地方使用[[nodiscard]]是什么原因?”。
–克里斯蒂安·哈克(Christian Hackl)
19年8月19日在13:19
#4 楼
我对[[nodiscard]]的看法是,应将其用于可能无法获取结果而导致程序错误行为的地方,而不仅仅是一些多余的计算。这就是为什么,我不会将[[nodiscard]]放在吸气剂或纯函数的结果上。但是我会在结果的生命周期中使用那些功能。类似
malloc
的函数会浮现在脑海,但是大多数函数很可能也会返回一个新的唯一指针或其他RAII对象。在每个observable
上都可以附加一个任意的lambda:value.observe([](auto value) {...})
语义是-每当更改
value
的内容时,都会调用该lambda。问题是-observe
返回一个Handle
,该值控制lambda观察值的时间。手柄被破坏时,λ值已从观测值中脱离出来。很容易忘记保持手柄。但是,在没有有效保持结果的情况下调用
observe
不会产生任何效果-语义上的重大偏离,仅是轻微的无效。在此scenaro中,[[nodiscard]]
真是天赐之物,我相信这些都是应该使用它的场景。并非无处不在。#5 楼
大多数C ++编译器都有一个选项,该选项可在每次忽略返回的值时启用警告。例如,在g ++中,您可以使用-Wunused-result
来获取这些警告。已经添加了
[[nodiscard]]
属性,以使始终产生警告与不产生警告之间处于中间地带,这就是为什么大多数使用它的描述都需要选择它的方法。
评论
嗯,有很多函数可以合理地放弃返回值。例如,运算符=。还有std :: map :: insert。@immibis:是的。标准库充满了此类功能。但是在我自己的代码中,它们往往极为罕见。您认为这是库代码还是应用程序代码的问题?
“ ...太冗长了,它开始感觉像Java ...”-在有关C ++的问题中读到此内容使我有点抽搐:-/
@ChristianHackl评论可能不是“猛烈抨击”的正确地方,当然,与其他语言相比,Java也很冗长。但是头文件,赋值运算符,复制构造函数和const的正确用法在C ++中可能会大大鼓吹否则为“简单”的类(或更确切地说为“纯旧数据对象”)。
@ Marco13:我无法在一条评论中解决太多问题。简而言之:Java没有头文件,它减少了文件数,但增加了耦合。 OTOH迫使您将每个顶级类放入其自己的文件中,并将每个函数放入一个类中。在C ++中,您几乎永远不会自己实现赋值运算符和复制构造函数。这些函数与标准库类(例如std :: vector或std :: unique_ptr)更相关,在类定义中,您只需要数据成员即可。我曾经用过两种语言。 Java是一种不错的语言,但是比较冗长。