为什么在新代码中几乎不使用C ++ 17的[[nodiscard]]是什么原因?


70

C ++ 17引入了该[[nodiscard]]属性,该属性允许程序员以一种方式标记函数,如果返回的对象被调用者丢弃,则编译器将生成警告。可以将相同的属性添加到整个类类型。

我已经在最初的提案中了解了使用此功能的动机,并且我知道C ++ 20会将属性添加到类似的标准函数中std::vector::empty,其名称并未传达关于返回值的明确含义。

这是一个很酷的实用功能。实际上,这似乎有用了。在我所读过的所有地方[[nodiscard]],人们都在讨论它,就好像您只是将其添加到少数几个函数或类型中而忽略了其余的函数或类型一样。但是,为什么不可丢弃的值应该是特例,尤其是在编写新代码时?丢弃的返回值通常不是bug还是至少浪费资源?

C ++本身的设计原则之一不是编译器应该捕获尽可能多的错误吗?

如果是这样,那么为什么不将[[nodiscard]]您自己的非传统代码添加到几乎每个单个非void函数和几乎每个单个类类型中呢?

我已经尝试过用自己的代码来做到这一点,并且它工作得很好,除了它非常冗长,以至于开始看起来像Java。使编译器默认警告丢弃的返回值 似乎更自然,除非您标记意图[*]的其他少数情况。

在标准提案,博客条目,Stack Overflow问题或互联网上的其他任何地方,我对这种可能性的讨论都为零,我一定会错过一些东西。

为什么这样的机制在新的C ++代码中没有意义?冗长是不是[[nodiscard]]几乎无处不在的唯一理由吗?


[*]从理论上讲,您可能拥有类似[[maydiscard]]属性的内容,也可以将其追溯地添加到printf标准库实现中的功能中。


22
嗯,有很多函数可以合理地放弃返回值。operator =例如。和std::map::insert
immibis

2
@immibis:是的。标准库充满了此类功能。但是在我自己的代码中,它们往往极为罕见。您认为这是库代码还是应用程序代码的问题?
Christian Hackl

9
“ ...非常冗长,它开始看起来像Java ...” -在有关C ++的问题中读到此内容使我有点抽搐:-/
Marco13年

3
@ChristianHackl评论可能不是“猛烈抨击”的正确地方,当然,与其他语言相比,Java也很冗长。但头文件,赋值运算符,拷贝构造和的正确用法const可以臃肿否则“简单”类(或者更确切地说,“普通老式数据对象”)显着地在C ++中。
Marco13 '18

2
@ Marco13:我无法在一条评论中解决这么多问题。简而言之:Java没有头文件,它减少了文件数,但增加了耦合。OTOH迫使您将每个顶级类放入其自己的文件中,并将每个函数放入一个类中。在C ++中,您几乎永远不会自己实现赋值运算符和复制构造函数。这些功能与标准库类(例如std::vector或)更相关,std::unique_ptr在类定义中,您只需要数据成员即可。我曾经用过两种语言。Java是一种不错的语言,但是更加冗长。
Christian Hackl

Answers:


73

在不需要与旧标准兼容的新代码中,请在合理的地方使用该属性。但是对于C ++,[[nodiscard]]默认设置很差。您建议:

使编译器默认警告丢弃的返回值似乎更为自然,除非您标记了意图的其他几种情况。

那会突然导致现有的正确代码发出很多警告。尽管由于任何现有代码仍可以成功编译,因此从技术上讲这种更改可以被认为是向后兼容的,但实际上这将是语义上的巨大变化。

具有非常大的代码库的现有成熟语言的设计决策必然不同于全新语言的设计决策。如果这是一种新语言,则默认情况下发出警告是明智的。例如,Nim语言要求将不需要的值显式丢弃-这类似于将C ++中的每个expression语句都用cast包裹起来(void)(...)

一个[[nodiscard]]属性是在两种情况下最有用:

  • 如果函数除了返回特定结果外没有其他效果,即为纯函数。如果未使用结果,则该调用肯定是无用的。另一方面,丢弃结果将是不正确的。

  • 是否必须检查返回值,例如对于返回错误代码而不是抛出错误的类C接口。这是主要的用例。对于惯用的C ++,这将非常罕见。

这两种情况留下了不纯函数的巨大空间,这些函数会返回一个值,在这种情况下,这种警告会产生误导。例如:

  • 考虑一种队列数据类型,该数据类型具有.pop()删除元素并返回已删除元素的副本的方法。这种方法通常很方便。但是,在某些情况下,我们只希望删除元素而不获取副本。那是完全合法的,警告将无济于事。不同的设计(例如std::vector)将这些职责分开,但这还有其他折衷。

    请注意,在某些情况下,无论如何都必须制作副本,因此感谢RVO返回,副本是免费的。

  • 考虑流畅的接口,其中每个操作都返回对象,以便可以执行进一步的操作。在C ++中,最常见的示例是流插入运算符<<[[nodiscard]]向每个<<重载添加属性将非常麻烦。

这些示例表明,使用“默认情况下不丢弃的C ++ 17”语言在没有警告的情况下使惯用的C ++代码编译起来将非常繁琐。

请注意,您闪亮的新C ++ 17代码(可以在其中使用这些属性)可能仍会与针对较早C ++标准的库一起编译。向后兼容性对于C / C ++生态系统至关重要。因此,将nodiscard设置为默认值将对典型的惯用例产生许多警告-如果不对库的源代码进行深远的更改,就无法解决这些警告。

可以说,这里的问题不是语义上的改变,而是每个C ++标准的功能都适用于每个编译单元范围,而不适用于每个文件范围。如果将来某些C ++标准不再使用头文件,则这种更改将更加现实。


6
我对此表示赞同,因为它包含有用的见解。不过,目前还不确定是否真的可以回答这个问题。诸如pop或的不纯函数operator<<将由我的虚构[[maydiscard]]属性显式标记,因此不会生成警告。你说这样的功能一般都更常见的比我愿意相信,或者更常见一般比在自己的代码库?您关于现有代码的第一段当然是对的,但仍然让我想知道是否还有其他人正在使用,如果不是,那么为什么不使用他们的所有代码[[nodiscard]]
Christian Hackl

23
@ChristianHackl:在C ++的历史中曾经有一段时间,将值作为const返回是一种很好的做法。你不能说returns_an_int() = 0,那为什么要returns_a_str() = ""工作呢?C ++ 11表明这实际上是一个可怕的想法,因为现在所有这些代码都禁止移动,因为值是const。我认为该课程的正确之处在于“呼叫者对结果的处理方式不取决于您”。忽略结果是呼叫者可能想要做的完全正确的事情,除非您知道这是错误的(很高的标准),否则您不应阻止它。
GManNickG

13
empty()因为名称非常含糊,所以使用nodiscard 也是明智的:它是谓词is_empty()还是mutator clear()?通常,您不会期望这样的增变器返回任何内容,因此,有关返回值的警告可能会提醒程序员出现问题。使用更清晰的名称,此属性将不是必需的。
阿蒙(Amon)

13
@ChristianHackl:正如其他人所说,我认为纯函数是何时使用此函数的一个很好的例子。我会更进一步,说应该有一个[[pure]]属性,它应该暗示[[nodiscard]]。这样,很清楚为什么不应该丢弃此结果。但是除了纯函数之外,我无法想到应该具有这种行为的另一类函数。而且大多数功能不是纯函数,因此我认为nodiscard作为默认值不是正确的选择。
GManNickG

5
@GManNickG:尝试并未能调试忽略错误代码的代码后,我坚信默认情况下需要检查绝大多数错误代码。
Mooing Duck's

9

我不会[[nodiscard]]遍地开花的原因:

  1. (主要:)这将介绍的方式在我的头太多的噪音。
  2. (主要:)我不认为我应该对其他人的代码做出强烈的推测性假设。您想丢弃我给您的返回值吗?好吧,把自己踢出去。
  3. (未成年人:)您将保证与C ++ 14不兼容

现在,如果将其设置为默认值,则将强制所有库开发人员强制其所有用户不放弃返回值。那太可怕了。或者您强迫他们添加[[maydiscard]]无数个功能,这也很可怕。


0

例如:operator <<的返回值取决于绝对必要或绝对无用的调用。(std :: cout << x << y,第一个是必需的,因为它返回流,第二个完全没有用)。现在与printf进行比较,在这里每个人都丢弃返回的值以进行错误检查,但是操作符<<没有错误检查,因此它起初没有那么有用。因此,在两种情况下,丢弃都只会造成破坏。


这回答了一个没有被问到的问题,即“什么[[nodiscard]]地方不使用的原因是什么?”。
Christian Hackl
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.