web-dev-qa-db-ja.com

参照されていないすべてのポインタをnullガードすることは理にかなっていますか?

新しい仕事では、次のようなコードのコードレビューでフラグが付けられています。

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender) { }

void PowerManager::SignalShutdown()
{
    msgSender_->sendMsg("shutdown()");
}

私は最後の方法が読むべきだと言われました:

void PowerManager::SignalShutdown()
{
    if (msgSender_) {
        msgSender_->sendMsg("shutdown()");
    }
}

つまり、I mustmsgSender_変数は、プライベートデータメンバーであるにもかかわらず、NULLガードを囲みます。私がこの「知恵」について私がどのように感じているかを説明するのに虚辞を使うことを自分自身に制限するのは難しいです。説明を求めると、一部のジュニアプログラマーが、クラスがどのように機能していたのかについて混乱し、必要のないメンバーを誤って削除してしまった(そして、NULLに設定した)ことについてのホラーストーリーがたくさんあります。その後、明らかに)、そして製品リリースの直後に現場で物事が爆発しました、そして私たちは単に「NULL check everything」の方が良いことを「ハードな方法を学び、私たちを信頼しました」。

私には、これは 貨物カルトプログラミング のように感じられ、単純明快です。何人かの善意の同僚が私が「それを手に入れる」のを助け、これがどのようにしてより堅牢なコードを書くのに役立つかを見ようと真剣に取り組んでいます... 。

関数内で逆参照されたevery singleポインターが最初にNULLをチェックする必要があること、つまりプライベートデータメンバーであっても、コーディング標準が要求することは合理的ですか? (注:状況を説明するために、航空管制システムやその他の「故障と等しい人と死ぬ」製品ではなく、民生用電子機器を製造しています。)

[〜#〜] edit [〜#〜]:上記の例では、msgSender_コラボレーターはオプションではありません。 NULLである場合は、バグを示しています。これがコンストラクタに渡される唯一の理由は、PowerManagerをモックIMsgSenderサブクラスでテストできるためです。

[〜#〜]要約[〜#〜]:この質問には本当に素晴らしい回答がありました。皆さんに感謝します。 @aaronpsからのものを受け入れたのは、その簡潔さのためです。次のようなかなり一般的な合意があるようです。

  1. every single逆参照されたポインタにNULLガードを必須にすることは過剰ですが、
  2. 代わりに参照(可能であれば)またはconstポインターを使用して、ディベート全体を回避できます。
  3. assertステートメントは、関数の前提条件が満たされていることを確認するためのNULLガードのより賢明な代替手段です。
66
evadeflow

それは「契約」に依存します:

PowerManager[〜#〜] must [〜#〜]に有効なIMsgSenderがある場合は、nullをチェックしないで、すぐに終了させます。

一方、それが[〜#〜] may [〜#〜]IMsgSenderがある場合、使用するたびに、そのように単純にチェックする必要があります。

後輩プログラマーの話についての最後のコメント、問題は実際にはテスト手順の欠如です。

66
aaronps

コードを読むべきだと思います:

_PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender)
{
    assert(msgSender);
}

void PowerManager::SignalShutdown()
{
    assert(msgSender_);
    msgSender_->sendMsg("shutdown()");
}
_

_msgSender__がNULLの場合、関数が呼び出されてはならないことが明確になるため、これは実際にはNULLを保護するよりも優れています。また、これが発生した場合に通知されます。

同僚の「知恵」を共有すると、このエラーは黙って無視され、予期しない結果が生じます。

一般に、バグは原因の近くで検出されれば、修正が簡単です。この例では、提案されているNULLガードにより、シャットダウンメッセージが設定されず、顕著なバグが発生する場合とそうでない場合があります。 SignalShutdown関数に逆方向に作業するのは、アプリケーション全体が終了したばかりで、SignalShutdown()を直接指している便利なバックトレースまたはコアダンプを生成する場合よりも困難です。

これは少し直観に反しますが、何かが間違っているとすぐにクラッシュすると、コードがより堅牢になる傾向があります。それはあなたが実際にfind問題を抱えているためであり、非常に明白な原因もある傾向があります。

77
Kristof Provost

MsgSender 絶対にしないでくださいnullの場合、nullチェックをコンストラクターにのみ配置する必要があります。これは、クラスへの他のすべての入力にも当てはまります。「モジュール」へのエントリの時点で整合性チェックを行います-クラス、関数など。

私の経験則では、モジュール境界(この場合はクラス)間で整合性チェックを実行します。さらに、クラスは、クラスメンバーのライフタイムの整合性を迅速に精神的に検証できるように十分に小さくする必要があります。不適切な削除やnull割り当てなどの間違いを防ぐことができます。投稿のコードで実行されるnullチェックは、無効な使用法が実際にポインタにnullを割り当てることを前提としています-これは常にそうであるとは限りません。 「無効な使用法」は本質的に通常のコードに関する仮定が適用されないことを意味するため、すべてのタイプのポインターエラーを確実にキャッチすることはできません。たとえば、無効な削除、増分などです。

さらに、引数がnullになることのないことが確実な場合は、クラスの使用方法に応じて、参照の使用を検討してください。それ以外の場合は、生のポインタの代わりにstd::unique_ptrまたはstd::shared_ptrの使用を検討してください。

30
Max

いいえ、チェックすることは妥当ではありませんeach and everyポインター逆参照がNULLであることを確認します。

Nullポインターチェックは、関数の引数(コンストラクターの引数を含む)で、前提条件が満たされていることを確認したり、オプションのパラメーターが指定されていない場合に適切なアクションを実行したりするのに役立ちます。また、クラスの内部を公開した後でクラスの不変条件をチェックするのに役立ちます。しかし、ポインタがNULLになった唯一の理由がバグの存在である場合、チェックする意味がありません。そのバグは、ポインタを別の無効な値に設定するのと同じくらい簡単でした。

私があなたのような状況に直面した場合、私は2つの質問をします。

  • そのジュニアプログラマーの間違いがリリース前に発見されなかったのはなぜですか?たとえば、コードレビューやテスト段階ですか?欠陥のある家庭用電化製品を市場に出すことは、安全性が重視されるアプリケーションで使用されている欠陥のあるデバイスをリリースすることと同じくらいコストがかかる可能性があります(市場シェアと信用を考慮すると)。その他のQA活動。
  • Nullチェックが失敗した場合、どのエラー処理を行うと思いますか、またはnullチェックの代わりにassert(msgSender_)と書くだけですか? nullチェックを入れただけでは、クラッシュは防げたかもしれませんが、実際には操作がスキップされているにもかかわらず操作が行われたという前提でソフトウェアが続行しているため、さらに悪い状況が発生した可能性があります。これにより、ソフトウェアの他の部分が不安定になる可能性があります。

この例は、入力パラメーターがnullであるかどうかよりも、オブジェクトの有効期間についてのようです†。 PowerManageralwaysが有効なIMsgSenderでなければならないことを述べたので、引数によってポインターで渡す(それによってnullポインターの可能性を許可する)と、設計として私に印象を与えます欠陥††。

このような状況では、呼び出し側の要件が言語によって強制されるように、インターフェースを変更したいと思います。

_PowerManager::PowerManager(const IMsgSender& msgSender)
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    msgSender_->sendMsg("shutdown()");
}
_

このように書き直すと、PowerManagerは、存続期間全体にわたってIMsgSenderへの参照を保持する必要があると言えます。これはまた、IMsgSenderPowerManagerよりも長く存続する必要があるという暗黙の要件を確立し、PowerManager内のnullポインターチェックまたはアサーションの必要性を否定します。

(boostまたはc ++ 11を介して)スマートポインターを使用して同じことを記述し、IMsgSenderPowerManagerよりも長く存続させることを明示的に強制することもできます。

_PowerManager::PowerManager(std::shared_ptr<IMsgSender> msgSender) 
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    // Here, we own a smart pointer to IMsgSender, so even if the caller
    // destroys the original pointer, we still have a valid copy
    msgSender_->sendMsg("shutdown()");
}
_

IMsgSenderの寿命がPowerManagerの寿命より長くなることが保証できない可能性がある場合(つまり、x = new IMsgSender(); p = new PowerManager(*x);)は、この方法が推奨されます。

†ポインターに関して:横行するnullチェックはコードを読みにくくし、安定性を向上させません(安定性のappearanceを向上させますが、はるかに悪いです)。

どこかで、誰かがIMsgSenderを保持するためのメモリのアドレスを取得しました。割り当てが成功したことを確認する(ライブラリの戻り値を確認するか、_std::bad_alloc_例外を適切に処理する)ことは、その関数の責任であり、無効なポインタを迂回しないようにします。

PowerManagerIMsgSenderを所有していないため(しばらくそれを借用しているだけです)、そのメモリの割り当てや破棄は行いません。これは、私が参照を好むもう1つの理由です。

††この仕事は初めてなので、既存のコードをハッキングしていると思います。つまり、設計上の欠陥とは、作業しているコードに欠陥があるということです。したがって、nullポインターをチェックしていないためにコードにフラグを付けている人々は、実際にはポインターを必要とするコードを記述していることにフラグを立てています:)

9
Seth

例外と同様に、ガード条件は、エラーから回復する方法を知っている場合、またはより意味のある例外メッセージを表示したい場合にのみ役立ちます。

エラー(例外またはガードチェックとしてキャッチされたかどうか)を飲み込むことは、エラーが問題ではないときに行うべき正しいことです。エラーが飲み込まれているのを確認する最も一般的な場所は、エラーログコードです。ステータスメッセージをログに記録できなかったため、アプリをクラッシュさせたくありません。

関数が呼び出されるときはいつでも、それはオプションの動作ではなく、静かにではなく、大声で失敗するはずです。

編集:ジュニアプログラマストーリーについて考えると、プライベートメンバーがnullに設定されていて、それが決して許可されていないことが原因のようです。彼らは不適切な書き込みに問題があり、読み取り時に検証することでそれを修正しようとしています。これは逆です。あなたがそれを特定するときまでに、エラーはすでに起こっています。そのためのコードレビュー/コンパイラの強制可能なソリューションは、ガード条件ではなく、ゲッターとセッターまたはconstメンバーです。

8
jmoreno

他の人が指摘したように、これはmsgSender合法的にNULLであるかどうかに依存します。以下は、neverがNULLであることを前提としています。

void PowerManager::SignalShutdown()
{
    if (!msgSender_)
    {
       throw SignalException("Shut down failed because message sender is not set.");
    }

    msgSender_->sendMsg("shutdown()");
}

チームの他の人が提案する「修正」は、 デッドプログラムは嘘をつかない の原則に違反しています。バグはそのままでは本当に見つけるのが難しいです。以前の問題に基づいて静かに動作を変更する方法は、最初のバグを見つけるのを難しくするだけでなく、独自の2番目のバグも追加します。

ジュニアはヌルをチェックしないことで大混乱を引き起こしました。このコードが未定義の状態で実行し続けることによって大混乱を引き起こした場合はどうなりますか(デバイスはオンですが、プログラムはオフであると「考えます」)。おそらく、プログラムの別の部分は、デバイスの電源が入っていないときにのみ安全なことを行います。

これらのアプローチのいずれかは、サイレント障害を回避します。

  1. この答え で提案されているようにアサートを使用しますが、本番用コードでオンになっていることを確認してください。もちろん、他のアサーションが本番環境でオフになると想定して記述されている場合、これにより問題が発生する可能性があります。

  2. Nullの場合、例外をスローします。

7
poke

コンストラクターでnullをトラップすることに同意します。さらに、メンバーがヘッダーで次のように宣言されている場合:

IMsgSender* const msgSender_;

その後、初期化後にポインタを変更することはできません。そのため、構築に問題がなければ、それを含むオブジェクトの存続期間中は問題ありません。 (ポイントされたオブジェクトtonotになります)

5

これはあからさまです危険です!

私はCコードベースの上級開発者の下で、同じことを要求する最も粗末な「標準」を使用して、すべてのポインターが盲目的にnullかどうかをチェックしました。開発者は次のようなことをすることになります:

// Pre: vertex should never be null.
void transform_vertex(Vertex* vertex, ...)
{
    // Inserted by my "wise" co-worker.
    if (!vertex)
        return;
    ...
}

私はかつて、そのような関数で前提条件のそのようなチェックを1回削除し、それをassertに置き換えて何が起こるかを確認しようとしました。

恐ろしいことに、この関数にnullを渡していたコードベースの数千行のコードを見つけましたが、開発者は混乱しがちでしたが、問題が解決するまで回避してコードを追加しました。

さらに恐ろしいことに、この問題はnullをチェックするコードベースのあらゆる場所で蔓延していることがわかりました。コードベースは何十年にもわたって成長し、最も明確に文書化された前提条件でさえも黙って違反できるようにするために、これらのチェックに依存するようになりました。これらの致命的なチェックを削除してassertsを支持することにより、コードベースでの数十年にわたるすべての論理的なヒューマンエラーが明らかになり、私たちはそれらに溺れます。

このように一見無害であるように見えるコードの2行+時間と、チームが蓄積された1000個のバグをマスクするだけで済みました。

これらは、ソフトウェアが機能するために存在する他のバグにバグを依存させる種類のプラクティスです。 悪夢のシナリオです。これらのnullチェックはすべてバグを非表示にし、忘れた場所に到達するまでバグを非表示にするため、このような前提条件の違反に関連するすべての論理エラーが、ミスが発生した実際のサイトから100万行のコードを不思議なほど表示します。バグを非表示にします。

ソフトウェアがアサーションの失敗や本番環境のクラッシュに対してミッションクリティカルであり、このシナリオの可能性が望ましい場合を除いて、ヌルポインターが前提条件に違反するすべての場所で盲目的にヌルをチェックすることは、まったく狂気です。

コーディング標準が、関数で逆参照されたすべての単一のポインターが最初にNULLかどうかをチェックすることを要求するのは妥当ですか?

ですから、絶対にそうではありません。 「安全」でもありません。それは正反対で、コードベース全体のあらゆる種類のバグを覆い隠す可能性があり、長年にわたって、最も恐ろしいシナリオにつながる可能性があります。

assertはここに行く方法です。前提条件の違反が見過ごされないようにする必要があります。そうしないと、マーフィーの法則が簡単に適用されてしまいます。

3
user204677

Objective-C は、たとえば、nilオブジェクトに対するすべてのメソッド呼び出しを、ゼロっぽい値に評価されるno-opとして扱います。質問で提案されている理由により、Objective-Cでの設計決定にはいくつかの利点があります。すべてのメソッド呼び出しをnullガードするという理論的な概念は、十分に公表され、一貫して適用されている場合、いくつかのメリットがあります。

とはいえ、コードは真空ではなくエコシステムに存在します。 nullで保護された動作は慣用的ではなく、C++では驚きであるため、有害であると見なす必要があります。要約すると、誰もそのようにC++コードを記述しないので、しないでくださいそれを行う!ただし、反例として、CおよびC++でdeleteに対してfree()またはNULLを呼び出すと、ノーオペレーションになることが保証されていることに注意してください。


あなたの例では、msgSenderがnullでないことをコンストラクタにアサートすることはおそらく価値があります。コンストラクターmsgSenderのメソッドをすぐに呼び出した場合は、とにかくクラッシュするため、そのようなアサーションは必要ありません。ただし、これは単にstoringmsgSenderであるため、将来の使用のために、SignalShutdown()のスタックトレースを見ても、値はNULLになるため、コンストラクターのアサーションによりデバッグが大幅に容易になります。

さらに良いことに、コンストラクタはconst IMsgSender&参照を受け入れる必要がありますが、NULLにすることはできません。

2
200_success

referenceの代わりにpointerを使用すると、msgSenderoptionそしてここではnullチェックが正しいでしょう。コードスニペットは、それを決定するには遅すぎます。多分PowerManagerには他にも価値がある(またはテスト可能な)要素があります...

ポインターとリファレンスのどちらかを選択するときは、両方のオプションを完全に比較検討します。 (プライベートメンバーの場合でも)メンバーにポインターを使用する必要がある場合は、逆参照するたびにif (x)プロシージャを受け入れる必要があります。

1
Wolf

Null逆参照を回避するよう求められる理由は、コードの堅牢性を確保するためです。はるか昔のジュニアプログラマの例は、単なる例です。誰もが誤ってコードを壊し、null逆参照を引き起こす可能性があります-特にグローバルとクラスグローバルの場合。 CおよびC++では、直接メモリ管理機能を使用して、偶然により多くの可能性があります。驚くかもしれませんが、このようなことが頻繁に起こります。非常に知識が豊富で、経験豊富で、上級の開発者でさえ。

すべてをnullチェックする必要はありませんが、nullになる可能性がかなり高い逆参照から保護する必要があります。これは通常、それらが異なる関数で割り当てられ、使用され、逆参照される場合です。他の関数の1つが変更され、関数が壊れる可能性があります。他の関数の1つが順不同で呼び出される可能性もあります(デストラクタとは別に呼び出すことができるデアロケータがある場合など)。

私はあなたの同僚がアサートの使用と組み合わせてあなたに言っているアプローチを好みます。テスト環境でクラッシュするため、修正して本番環境で適切に失敗する問題があることがより明確になります。

また、coverityやfortifyなどの堅牢なコード正確性ツールを使用する必要があります。そして、すべてのコンパイラ警告に対処する必要があります。

編集:他の人が述べたように、いくつかのコード例のように、静かに失敗することも一般的に間違っています。関数がnullの値から回復できない場合は、呼び出し元にエラーを返す(または例外をスローする)必要があります。呼び出し元は、呼び出し順序を修正するか、回復するか、または呼び出し元にエラーを返す(または例外をスローする)などの責任があります。最終的には、関数が正常に回復して次に進む、正常に回復して失敗する(1人のユーザーの内部エラーのためにデータベースがトランザクションに失敗したが、実際には終了していない)か、関数がアプリケーションの状態が破損していると判断すると回復不能とアプリケーションが終了します。

1
atk

それはあなたが何をしたいかに依存します。

あなたは「家電機器」に取り組んでいると書いています。どういうわけか、msgSender_NULLに設定してバグが発生した場合、

  • 続行するデバイス、SignalShutdownをスキップして残りの操作を続行する、または
  • デバイスがクラッシュし、ユーザーに強制的に再起動させますか?

送信されないシャットダウン信号の影響に応じて、オプション1mightが実行可能な選択肢になります。ユーザーが引き続き音楽を聴くことができるが、ディスプレイにまだ前のトラックのタイトルが表示されている場合、その完全なクラッシュよりもmightの方が望ましいデバイス。

もちろん、オプション1を選択した場合、assert(他の人が推奨)はvitalであり、このようなバグの可能性を減らします開発中に気付かれずに忍び寄ります。 if nullガードは、運用環境での障害を軽減するためだけにあります。

個人的には、プロダクションビルドでは「クラッシュアーリー」アプローチも好みますが、バグが発生した場合に簡単に修正および更新できるビジネスソフトウェアを開発しています。家庭用電化製品の場合、これはそれほど簡単ではないかもしれません。

0
Heinzi