assert マクロを再定義するのは悪いことですか?
一部の人々は、既存の標準の assert(cond) マクロを再定義するのではなく、独自のマクロ ASSERT(cond) を使用することを推奨しています。しかし、これは、assert() を使用する多くのレガシー コードがあり、ソース コードを変更したくない場合や、アサーション レポートをインターセプト、正規化したい場合には役に立ちません。
私はやった
#undef assert
#define assert(cond) ... my own assert code ...
上記のような状況で - すでにアサートを使用しているコードで、アサート失敗の動作を拡張したい - 次のようなことをしたいとき
1) アサーションをより便利にするために追加のエラー情報を出力する
2) アサート時にデバッガーまたはスタック トラックを自動的に呼び出す
... これ 2) は、SIGABRT シグナル ハンドラを実装することで、assert を再定義せずに実行できます。
3) アサーションの失敗をスローに変換する。
... これは、3)、シグナル ハンドラーでは実行できません。シグナル ハンドラーから C++ 例外をスローできないためです。(少なくとも確実ではありません。)
アサート スローを行う必要があるのはなぜですか? 積み重ねられたエラー処理。
私がこれを後者にするのは、通常、アサーションの後もプログラムを実行し続けたいからではなく (以下を参照)、例外を使用してエラーに関するより良いコンテキストを提供するのが好きだからです。私はよくします:
int main() {
try { some_code(); }
catch(...) {
std::string err = "exception caught in command foo";
std::cerr << err;
exit(1);;
}
}
void some_code() {
try { some_other_code(); }
catch(...) {
std::string err = "exception caught when trying to set up directories";
std::cerr << err;
throw "unhandled exception, throwing to add more context";
}
}
void some_other_code() {
try { some_other2_code(); }
catch(...) {
std::string err = "exception caught when trying to open log file " + logfilename;
std::cerr << err;
throw "unhandled exception, throwing to add more context";
}
}
等
つまり、例外ハンドラはもう少しエラー コンテキストを追加してから再スローします。
ときどき、stderr などに例外ハンドラを出力することがあります。
例外ハンドラをエラー メッセージのスタックにプッシュすることがあります。(明らかに、問題がメモリ不足の場合は機能しません。)
** これらのアサート例外はまだ終了しています ... **
この投稿にコメントした誰か、@IanGoldby は、「アサートが終了しないという考えは、私には意味がありません」と述べました。
私が明確ではなかったので、私は通常、そのような例外を終了させます。しかし、最終的には、おそらくすぐにはできません。
たとえば、代わりに
#include <iostream>
#include <assert.h>
#define OS_CYGWIN 1
void baz(int n)
{
#if OS_CYGWIN
assert( n == 1 && "I don't know how to do baz(1) on Cygwin). Should not call baz(1) on Cygwin." );
#else
std::cout << "I know how to do baz(n) most places, and baz(n), n!=1 on Cygwin, but not baz(1) on Cygwin.\n";
#endif
}
void bar(int n)
{
baz(n);
}
void foo(int n)
{
bar(n);
}
int main(int argc, char** argv)
{
foo( argv[0] == std::string("1") );
}
生産のみ
% ./assert-exceptions
assertion "n == 1 && "I don't know how to do baz(1) on Cygwin). Should not call baz(1) on Cygwin."" failed: file "assert-exceptions.cpp", line 9, function: void baz(int)
/bin/sh: line 1: 22180 Aborted (core dumped) ./assert-exceptions/
%
あなたがするかもしれない
#include <iostream>
//#include <assert.h>
#define assert_error_report_helper(cond) "assertion failed: " #cond
#define assert(cond) {if(!(cond)) { std::cerr << assert_error_report_helper(cond) "\n"; throw assert_error_report_helper(cond); } }
//^ TBD: yes, I know assert needs more stuff to match the definition: void, etc.
#define OS_CYGWIN 1
void baz(int n)
{
#if OS_CYGWIN
assert( n == 1 && "I don't know how to do baz(1) on Cygwin). Should not call baz(1) on Cygwin." );
#else
std::cout << "I know how to do baz(n) most places, and baz(n), n!=1 on Cygwin, but not baz(1) on Cygwin.\n";
#endif
}
void bar(int n)
{
try {
baz(n);
}
catch(...) {
std::cerr << "trying to accomplish bar by baz\n";
throw "bar";
}
}
void foo(int n)
{
bar(n);
}
int secondary_main(int argc, char** argv)
{
foo( argv[0] == std::string("1") );
}
int main(int argc, char** argv)
{
try {
return secondary_main(argc,argv);
}
catch(...) {
std::cerr << "main exiting because of unknown exception ...\n";
}
}
少し意味のあるエラーメッセージを取得します
assertion failed: n == 1 && "I don't know how to do baz(1) on Cygwin). Should not call baz(1) on Cygwin."
trying to accomplish bar by baz
main exiting because of unknown exception ...
これらの状況依存エラー メッセージがより意味のある理由を説明する必要はありません。たとえば、ユーザーは、なぜ baz(1) が呼び出されているのか、まったくわからない場合があります。これは、プログラム エラーである可能性があります。cygwin では、cygwin_alternative_to_baz(1) を呼び出す必要がある場合があります。
しかし、ユーザーは「バー」が何であるかを理解するかもしれません。
はい: これは動作が保証されていません。しかし、さらに言えば、アサートがアボート ハンドラを呼び出すよりも複雑なことを行う場合、アサートが機能する保証はありません。
write(2,"error baz(1) has occurred",64);
さらに、それが機能することが保証されているわけではありません (この呼び出しには安全なバグがあります)。
たとえば、malloc または sbrk が失敗した場合。
アサート スローを行う必要があるのはなぜですか? テスト
私が assert を時々再定義するもう 1 つの大きな理由は、レガシ コード (assert を使用してエラーを通知するコード) の単体テストを作成することでした。
このコードがライブラリ コードの場合、try/catch を介して呼び出しをラップすると便利です。エラーが検出されたかどうかを確認し、続行します。
ああ、私はそれを認めたほうがいいかもしれません: 私は時々、このレガシー コードを書きました。そして、意図的に assert() を使用してエラーを通知しました。ユーザーが try/catch/throw を実行することに頼ることができなかったので、実際には、多くの場合、同じコードを C/C++ 環境で使用する必要がありました。私は独自の ASSERT マクロを使いたくありませんでした。なぜなら、信じられないかもしれませんが、ASSERT はしばしば競合するからです。FOOBAR_ASSERT() と A_SPECIAL_ASSERT() が散らばっているコードは醜いと思います。いいえ...単に assert() を単独で使用するのはエレガントで、基本的に機能します。また、拡張することもできます.... assert() をオーバーライドしても問題ない場合。
とにかく、assert() を使用するコードが私のものであろうと他の誰かのものであろうと、SIGABRT または exit(1) を呼び出してコードを失敗させたい場合や、コードをスローさせたい場合があります。
exit(a) または SIGABRT で失敗するコードをテストする方法を知っています - のようなもの
for all tests do
fork
... run test in child
wait
check exit status
しかし、このコードは遅いです。常に携帯できるわけではありません。多くの場合、実行速度は数千倍遅くなります
for all tests do
try {
... run test in child
} catch (... ) {
...
}
これは、操作を続行する可能性があるため、エラー メッセージ コンテキストをスタックするよりもリスクが高くなります。ただし、キャッチする例外の種類はいつでも選択できます。
メタ観察
私は Andrei Alexandresciu と同じ考えで、セキュリティを確保したいコードのエラーを報告する最もよく知られた方法は例外であると考えています。(プログラマーがエラーリターンコードのチェックを忘れることができないためです。)
これが正しければ ... エラー報告に exit(1)/signals/ から例外へのフェーズ変更がある場合 ... レガシーコードをどのように使用するかという問題がまだ残っています。
そして、全体として、いくつかのエラー報告スキームがあります。異なるライブラリが異なるスキームを使用している場合、それらをどのように共存させるか。