新しいコードのほぼすべての場所でC ++ 17の[[nodiscard]]を使用しない理由は何ですか?


70

C ++ 17には[[nodiscard]]属性が導入されており、プログラマーは、返されたオブジェクトが呼び出し元によって破棄された場合にコンパイラーが警告を生成する方法で関数をマークできます。同じ属性をクラスタイプ全体に追加できます。

私は元の提案でこの機能の動機について読んだことがあり、C ++ 20が属性をのような標準関数に追加することを知っていますstd::vector::empty。その名前は戻り値に関する明確な意味を伝えません。

これはクールで便利な機能です。実際、それはほとんど便利すぎるようです。私が読んだ[[nodiscard]]すべての場所で、人々はあなたがそれを選択したいくつかの関数または型に追加し、残りを忘れるかのように議論しています。しかし、特に新しいコードを書くとき、なぜ破棄不可能な値が特別な場合である必要がありますか?通常、破棄された戻り値はバグではなく、少なくともリソースの無駄ではありませんか?

そして、コンパイラができるだけ多くのエラーをキャッチする必要があるというのは、C ++自体の設計原則の1つではありませんか?

もしそうなら[[nodiscard]]、独自の非レガシーコードをほぼすべての非void関数とほぼすべてのクラスタイプに追加してみませんか?

私は自分のコードでそれをやろうとしましたが、非常に冗長で、Javaのように感じ始めることを除いて、うまく動作します。意図をマークする他のいくつかの場合を除いて、デフォルトで破棄された戻り値について コンパイラーに警告させるほうがはるかに自然に思えます[*]

標準提案、ブログエントリ、Stack Overflowの質問、またはインターネット上の他の場所では、この可能性に関する議論がゼロであるため、何かが足りないはずです。

新しいC ++コードでは、なぜそのような仕組みが意味をなさないのでしょうか?冗長性は、[[nodiscard]]ほとんどどこでも使用しない唯一の理由ですか?


[*]理論的には、[[maydiscard]]代わりに属性のようなものがありprintf、標準ライブラリの実装のように関数にさかのぼって追加することもできます。


22
さて、戻り値賢明に破棄できる関数はたくさんあります。operator =例えば。そしてstd::map::insert
user253751

2
@immibis:はい。標準ライブラリには、このような関数がいっぱいです。しかし、私自身のコードでは、それらは非常にまれな傾向があります。ライブラリコードとアプリケーションコードの問題だと思いますか?
クリスチャンハックル

9
「... Javaのように感じるようになるとひどく冗長になります...」 -C ++に関する質問でこれを読むと、私は少し痙攣しました:-/
Marco13

3
@ChristianHacklコメントは「言語バッシング」に適した場所ではない可能性が高く、もちろんJavaは他の言語と比較して冗長です。ただし、ヘッダーファイル、代入演算子、コピーコンストラクター、および適切な使用法は、C ++ constで「単純な」クラス(または「古いデータオブジェクト」)を大幅に増大させる可能性があります。
-Marco13

2
@ Marco13:1つのコメントで非常に多くの点に対処することはできません。簡単に説明しましょう。Javaにはヘッダーファイルがないため、ファイル数は減りますが、カップリングは増えます。OTOHでは、すべての最上位クラスを独自のファイルに入れ、すべての関数をクラスに入れます。C ++では、代入演算子を実装したり、コンストラクタを自分でコピーしたりすることはほとんどありません。これらの関数は、std::vectorまたはのような標準ライブラリクラスに関連std::unique_ptrがありますが、クラス定義にデータメンバーが必要なだけです。私は両方の言語で働いてきました。Javaは大丈夫な言語ですが、より冗長です。
クリスチャンハックル

回答:


73

古い標準との互換性を必要としない新しいコードでは、賢明な場合は常にその属性を使用してください。しかし、C ++の場合、[[nodiscard]]デフォルトは不適切です。あなたが提案する:

意図をマークする他のいくつかの場合を除いて、デフォルトで破棄された戻り値についてコンパイラーに警告させる方がはるかに自然に思えます。

これにより、既存の正しいコードが突然多くの警告を発します。既存のコードはすべて正常にコンパイルされるため、このような変更は技術的に下位互換性があると見なすことができますが、実際にはセマンティクスの大きな変更になります。

非常に大規模なコードベースを持つ既存の成熟した言語の設計決定は、完全に新しい言語の設計決定とは必然的に異なります。これが新しい言語である場合、デフォルトの警告は賢明です。たとえば、Nim言語では、不要な値を明示的に破棄する必要があります。これは、C ++のすべての式ステートメントをcastでラップすることに似てい(void)(...)ます。

[[nodiscard]]属性には、2つの場合に最も有用です:

  • 関数が特定の結果を返す以外の効果を持たない場合、つまり純粋な場合。結果が使用されない場合、呼び出しは確かに役に立たない。一方、結果を破棄することは正しくありません。

  • 戻り値をチェックする必要がある場合、たとえば、スローする代わりにエラーコードを返すCのようなインターフェイスの場合。これが主な使用例です。慣用的なC ++の場合、これは非常にまれです。

これらの2つのケースでは、値を返す不純な関数の巨大なスペースが残ります。例えば:

  • .pop()要素を削除し、削除された要素のコピーを返すメソッドを持つキューデータ型を検討します。そのような方法はしばしば便利です。ただし、コピーを取得せずに要素のみを削除したい場合があります。それは完全に合法であり、警告は役に立たないでしょう。別の設計(などstd::vector)はこれらの責任を分割しますが、それには他のトレードオフがあります。

    場合によってはコピーを作成する必要があるため、RVOのおかげでコピーが無料で返されることに注意してください。

  • 流fluentなインターフェイスを検討してください。各操作ではオブジェクトが返され、さらに操作を実行できます。C ++での最も一般的な例は、ストリーム挿入演算子<<です。[[nodiscard]]<<オーバーロードに属性を追加するのは非常に面倒です。

これらの例は、「デフォルトでnodiscardを使用したC ++ 17」言語で警告なしに慣用的なC ++コードをコンパイルするのは非常に退屈であることを示しています。

光沢のある新しいC ++ 17コード(これらの属性を使用できる場所)は、古いC ++標準を対象とするライブラリと一緒にコンパイルされる場合があります。この後方互換性は、C / C ++エコシステムにとって重要です。そのため、nodiscardをデフォルトにすると、一般的な慣用的な使用例に対して多くの警告が発生します。この警告は、ライブラリのソースコードに大幅な変更を加えなければ修正できません。

おそらく、ここでの問題はセマンティクスの変更ではなく、各C ++標準の機能がファイルごとのスコープではなく、コンパイル単位ごとのスコープに適用されることです。将来のC ++標準がヘッダーファイルから遠ざかる場合、そのような変更はより現実的です。


6
有益な洞察が含まれているため、これを支持しました。しかし、それが本当に質問に答えるかどうかはまだわかりません。popまたはのような不純な関数はoperator<<、想像上の[[maydiscard]]属性によって明示的にマークされるため、警告は生成されません。このような関数は、一般的に信じられないほど一般的であると言っていますか、それとも自分のコードベースよりも一般的であると言っていますか?既存のコードに関する最初の段落は確かに真実ですが、他の誰かが現在、すべての新しいコードを[[nodiscard]]でペッパー化しているかどうか疑問に思っています。
クリスチャンハックル

23
@ChristianHackl:C ++の歴史には、値をconstとして返すのが良い習慣と考えられていた時代がありました。と言うことはできませんreturns_an_int() = 0ので、なぜ動作するはずreturns_a_str() = ""ですか?C ++ 11は、これが実際にはひどいアイデアだと示しました。なぜなら、値がconstであるため、このコードはすべて移動を禁止しているからです。その教訓からの適切な教訓は、「発信者があなたの結果で何をするかはあなた次第ではない」と思います。結果を無視することは、呼び出し側が望むかもしれない完全に有効なことであり、それが間違っている(非常に高いバー)ことを知らない限り、ブロックするべきではありません。
GManNickG

13
empty()名前が非常に曖昧であるため、nodiscard on も意味があります。それは述語ですかis_empty()clear()それともミューテーターですか?通常、このようなミューテーターが何かを返すことは期待しないため、戻り値に関する警告はプログラマーに問題を警告することができます。より明確な名前では、この属性は必要ではありません。
アモン

13
@ChristianHackl:他の人が言ったように、私は純粋な関数がこれがいつ使われるべきかの良い例だと思います。さらに一歩進んで、[[pure]]属性があるべきだと言います[[nodiscard]]。そうすれば、なぜこの結果を破棄すべきではないのかが明確になります。しかし、純粋な関数以外に、この動作が必要な別のクラスの関数は考えられません。そして、ほとんどの関数は純粋ではないので、デフォルトとしてのnodiscardが正しい選択だとは思いません。
GManNickG

5
@GManNickG:エラーコードを無視するコードのデバッグを試みたが失敗したため、エラーコードの大部分はデフォルトでチェックする必要があると強く信じています。
Mooingダック

9

私が[[nodiscard]]ほとんどどこにもいなかった理由:

  1. (主要:)これは導入する方法私のヘッダにあまりにも多くのノイズを。
  2. (メジャー:)他の人のコードについて強い投機的な仮定を立てるべきだとは思わない。あなたは私があなたに与えた戻り値を捨てたいですか?いいよ、ノックアウト。
  3. (マイナー:) C ++ 14との非互換性を保証します。

ここで、デフォルトにした場合、すべてのライブラリ開発者に、すべてのユーザーに戻り値を破棄しないように強制することになります。それは恐ろしいことです。または[[maydiscard]]、無数に多くの関数を追加するように強制しますが、これも一種の恐ろしいことです。


0

例として:operator <<には、絶対に必要なコールまたは絶対に役に立たないコールに依存する戻り値があります。(std :: cout << x << y、最初のストリームはストリームを返すため必要であり、2番目はまったく役に立ちません)。ここで、エラーチェックのために存在する戻り値を全員が破棄するprintfと比較してください。したがって、どちらの場合も、nodiscardは損害を与えるだけです。


これは、尋ねられなかった質問、すなわち「[[nodiscard]]どこでも使わない理由は何ですか?」に答えます。
クリスチャンハックル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.