新しい仕事では、次のようなコードのコードレビューでフラグが付けられています。
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からのものを受け入れたのは、その簡潔さのためです。次のようなかなり一般的な合意があるようです。
NULL
ガードを必須にすることは過剰ですが、const
ポインターを使用して、ディベート全体を回避できます。assert
ステートメントは、関数の前提条件が満たされていることを確認するためのNULL
ガードのより賢明な代替手段です。それは「契約」に依存します:
PowerManager
[〜#〜] must [〜#〜]に有効なIMsgSender
がある場合は、nullをチェックしないで、すぐに終了させます。
一方、それが[〜#〜] may [〜#〜]にIMsgSender
がある場合、使用するたびに、そのように単純にチェックする必要があります。
後輩プログラマーの話についての最後のコメント、問題は実際にはテスト手順の欠如です。
コードを読むべきだと思います:
_PowerManager::PowerManager(IMsgSender* msgSender)
: msgSender_(msgSender)
{
assert(msgSender);
}
void PowerManager::SignalShutdown()
{
assert(msgSender_);
msgSender_->sendMsg("shutdown()");
}
_
_msgSender_
_がNULLの場合、関数が呼び出されてはならないことが明確になるため、これは実際にはNULLを保護するよりも優れています。また、これが発生した場合に通知されます。
同僚の「知恵」を共有すると、このエラーは黙って無視され、予期しない結果が生じます。
一般に、バグは原因の近くで検出されれば、修正が簡単です。この例では、提案されているNULLガードにより、シャットダウンメッセージが設定されず、顕著なバグが発生する場合とそうでない場合があります。 SignalShutdown
関数に逆方向に作業するのは、アプリケーション全体が終了したばかりで、SignalShutdown()
を直接指している便利なバックトレースまたはコアダンプを生成する場合よりも困難です。
これは少し直観に反しますが、何かが間違っているとすぐにクラッシュすると、コードがより堅牢になる傾向があります。それはあなたが実際にfind問題を抱えているためであり、非常に明白な原因もある傾向があります。
MsgSender 絶対にしないでくださいがnull
の場合、null
チェックをコンストラクターにのみ配置する必要があります。これは、クラスへの他のすべての入力にも当てはまります。「モジュール」へのエントリの時点で整合性チェックを行います-クラス、関数など。
私の経験則では、モジュール境界(この場合はクラス)間で整合性チェックを実行します。さらに、クラスは、クラスメンバーのライフタイムの整合性を迅速に精神的に検証できるように十分に小さくする必要があります。不適切な削除やnull割り当てなどの間違いを防ぐことができます。投稿のコードで実行されるnullチェックは、無効な使用法が実際にポインタにnullを割り当てることを前提としています-これは常にそうであるとは限りません。 「無効な使用法」は本質的に通常のコードに関する仮定が適用されないことを意味するため、すべてのタイプのポインターエラーを確実にキャッチすることはできません。たとえば、無効な削除、増分などです。
さらに、引数がnullになることのないことが確実な場合は、クラスの使用方法に応じて、参照の使用を検討してください。それ以外の場合は、生のポインタの代わりにstd::unique_ptr
またはstd::shared_ptr
の使用を検討してください。
いいえ、チェックすることは妥当ではありませんeach and everyポインター逆参照がNULL
であることを確認します。
Nullポインターチェックは、関数の引数(コンストラクターの引数を含む)で、前提条件が満たされていることを確認したり、オプションのパラメーターが指定されていない場合に適切なアクションを実行したりするのに役立ちます。また、クラスの内部を公開した後でクラスの不変条件をチェックするのに役立ちます。しかし、ポインタがNULLになった唯一の理由がバグの存在である場合、チェックする意味がありません。そのバグは、ポインタを別の無効な値に設定するのと同じくらい簡単でした。
私があなたのような状況に直面した場合、私は2つの質問をします。
assert(msgSender_)
と書くだけですか? nullチェックを入れただけでは、クラッシュは防げたかもしれませんが、実際には操作がスキップされているにもかかわらず操作が行われたという前提でソフトウェアが続行しているため、さらに悪い状況が発生した可能性があります。これにより、ソフトウェアの他の部分が不安定になる可能性があります。この例は、入力パラメーターがnullであるかどうかよりも、オブジェクトの有効期間についてのようです†。 PowerManager
はalwaysが有効なIMsgSender
でなければならないことを述べたので、引数によってポインターで渡す(それによってnullポインターの可能性を許可する)と、設計として私に印象を与えます欠陥††。
このような状況では、呼び出し側の要件が言語によって強制されるように、インターフェースを変更したいと思います。
_PowerManager::PowerManager(const IMsgSender& msgSender)
: msgSender_(msgSender) {}
void PowerManager::SignalShutdown() {
msgSender_->sendMsg("shutdown()");
}
_
このように書き直すと、PowerManager
は、存続期間全体にわたってIMsgSender
への参照を保持する必要があると言えます。これはまた、IMsgSender
がPowerManager
よりも長く存続する必要があるという暗黙の要件を確立し、PowerManager
内のnullポインターチェックまたはアサーションの必要性を否定します。
(boostまたはc ++ 11を介して)スマートポインターを使用して同じことを記述し、IMsgSender
をPowerManager
よりも長く存続させることを明示的に強制することもできます。
_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
_例外を適切に処理する)ことは、その関数の責任であり、無効なポインタを迂回しないようにします。
PowerManager
はIMsgSender
を所有していないため(しばらくそれを借用しているだけです)、そのメモリの割り当てや破棄は行いません。これは、私が参照を好むもう1つの理由です。
††この仕事は初めてなので、既存のコードをハッキングしていると思います。つまり、設計上の欠陥とは、作業しているコードに欠陥があるということです。したがって、nullポインターをチェックしていないためにコードにフラグを付けている人々は、実際にはポインターを必要とするコードを記述していることにフラグを立てています:)
例外と同様に、ガード条件は、エラーから回復する方法を知っている場合、またはより意味のある例外メッセージを表示したい場合にのみ役立ちます。
エラー(例外またはガードチェックとしてキャッチされたかどうか)を飲み込むことは、エラーが問題ではないときに行うべき正しいことです。エラーが飲み込まれているのを確認する最も一般的な場所は、エラーログコードです。ステータスメッセージをログに記録できなかったため、アプリをクラッシュさせたくありません。
関数が呼び出されるときはいつでも、それはオプションの動作ではなく、静かにではなく、大声で失敗するはずです。
編集:ジュニアプログラマストーリーについて考えると、プライベートメンバーがnullに設定されていて、それが決して許可されていないことが原因のようです。彼らは不適切な書き込みに問題があり、読み取り時に検証することでそれを修正しようとしています。これは逆です。あなたがそれを特定するときまでに、エラーはすでに起こっています。そのためのコードレビュー/コンパイラの強制可能なソリューションは、ガード条件ではなく、ゲッターとセッターまたはconstメンバーです。
他の人が指摘したように、これはmsgSender
が合法的にNULL
であるかどうかに依存します。以下は、neverがNULLであることを前提としています。
void PowerManager::SignalShutdown()
{
if (!msgSender_)
{
throw SignalException("Shut down failed because message sender is not set.");
}
msgSender_->sendMsg("shutdown()");
}
チームの他の人が提案する「修正」は、 デッドプログラムは嘘をつかない の原則に違反しています。バグはそのままでは本当に見つけるのが難しいです。以前の問題に基づいて静かに動作を変更する方法は、最初のバグを見つけるのを難しくするだけでなく、独自の2番目のバグも追加します。
ジュニアはヌルをチェックしないことで大混乱を引き起こしました。このコードが未定義の状態で実行し続けることによって大混乱を引き起こした場合はどうなりますか(デバイスはオンですが、プログラムはオフであると「考えます」)。おそらく、プログラムの別の部分は、デバイスの電源が入っていないときにのみ安全なことを行います。
これらのアプローチのいずれかは、サイレント障害を回避します。
この答え で提案されているようにアサートを使用しますが、本番用コードでオンになっていることを確認してください。もちろん、他のアサーションが本番環境でオフになると想定して記述されている場合、これにより問題が発生する可能性があります。
Nullの場合、例外をスローします。
コンストラクターでnullをトラップすることに同意します。さらに、メンバーがヘッダーで次のように宣言されている場合:
IMsgSender* const msgSender_;
その後、初期化後にポインタを変更することはできません。そのため、構築に問題がなければ、それを含むオブジェクトの存続期間中は問題ありません。 (ポイントされたオブジェクトtoはnotになります)
これはあからさまです危険です!
私は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
はここに行く方法です。前提条件の違反が見過ごされないようにする必要があります。そうしないと、マーフィーの法則が簡単に適用されてしまいます。
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
にすることはできません。
referenceの代わりにpointerを使用すると、msgSender
がoptionそしてここではnullチェックが正しいでしょう。コードスニペットは、それを決定するには遅すぎます。多分PowerManager
には他にも価値がある(またはテスト可能な)要素があります...
ポインターとリファレンスのどちらかを選択するときは、両方のオプションを完全に比較検討します。 (プライベートメンバーの場合でも)メンバーにポインターを使用する必要がある場合は、逆参照するたびにif (x)
プロシージャを受け入れる必要があります。
Null逆参照を回避するよう求められる理由は、コードの堅牢性を確保するためです。はるか昔のジュニアプログラマの例は、単なる例です。誰もが誤ってコードを壊し、null逆参照を引き起こす可能性があります-特にグローバルとクラスグローバルの場合。 CおよびC++では、直接メモリ管理機能を使用して、偶然により多くの可能性があります。驚くかもしれませんが、このようなことが頻繁に起こります。非常に知識が豊富で、経験豊富で、上級の開発者でさえ。
すべてをnullチェックする必要はありませんが、nullになる可能性がかなり高い逆参照から保護する必要があります。これは通常、それらが異なる関数で割り当てられ、使用され、逆参照される場合です。他の関数の1つが変更され、関数が壊れる可能性があります。他の関数の1つが順不同で呼び出される可能性もあります(デストラクタとは別に呼び出すことができるデアロケータがある場合など)。
私はあなたの同僚がアサートの使用と組み合わせてあなたに言っているアプローチを好みます。テスト環境でクラッシュするため、修正して本番環境で適切に失敗する問題があることがより明確になります。
また、coverityやfortifyなどの堅牢なコード正確性ツールを使用する必要があります。そして、すべてのコンパイラ警告に対処する必要があります。
編集:他の人が述べたように、いくつかのコード例のように、静かに失敗することも一般的に間違っています。関数がnullの値から回復できない場合は、呼び出し元にエラーを返す(または例外をスローする)必要があります。呼び出し元は、呼び出し順序を修正するか、回復するか、または呼び出し元にエラーを返す(または例外をスローする)などの責任があります。最終的には、関数が正常に回復して次に進む、正常に回復して失敗する(1人のユーザーの内部エラーのためにデータベースがトランザクションに失敗したが、実際には終了していない)か、関数がアプリケーションの状態が破損していると判断すると回復不能とアプリケーションが終了します。
それはあなたが何をしたいかに依存します。
あなたは「家電機器」に取り組んでいると書いています。どういうわけか、msgSender_
をNULL
に設定してバグが発生した場合、
SignalShutdown
をスキップして残りの操作を続行する、または送信されないシャットダウン信号の影響に応じて、オプション1mightが実行可能な選択肢になります。ユーザーが引き続き音楽を聴くことができるが、ディスプレイにまだ前のトラックのタイトルが表示されている場合、その完全なクラッシュよりもmightの方が望ましいデバイス。
もちろん、オプション1を選択した場合、assert
(他の人が推奨)はvitalであり、このようなバグの可能性を減らします開発中に気付かれずに忍び寄ります。 if
nullガードは、運用環境での障害を軽減するためだけにあります。
個人的には、プロダクションビルドでは「クラッシュアーリー」アプローチも好みますが、バグが発生した場合に簡単に修正および更新できるビジネスソフトウェアを開発しています。家庭用電化製品の場合、これはそれほど簡単ではないかもしれません。