15

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/ から例外へのフェーズ変更がある場合 ... レガシーコードをどのように使用するかという問題がまだ残っています。

そして、全体として、いくつかのエラー報告スキームがあります。異なるライブラリが異なるスキームを使用している場合、それらをどのように共存させるか。

4

3 に答える 3

11

標準マクロを再定義するのは見苦しい考えであり、動作が技術的に定義されていないことは確かですが、最終的にはマクロは単なるソース コードの置換であり、アサーションによってプログラムが終了する限り、どのように問題が発生するかを理解するのは困難です。 .

とはいえ、定義自体の後の翻訳単位内のコードが を再定義した場合、意図した置換が確実に使用されない可能性がありますassert

そうassertでないコードを代用するとexit、新しい問題が発生します。代わりに投げることについてのあなたの考えが失敗する可能性がある病理学的なエッジケースがあります。

int f(int n)
{
    try
    {
        assert(n != 0);
        call_some_library_that_might_throw(n);
    }
    catch (...)
    {
        // ignore errors...
    }
    return 12 / n;
}

上記の値 0 はn、正常なエラー メッセージでアプリケーションを停止するのではなく、アプリケーションのクラッシュを開始します。スローされたメッセージの説明は表示されません。

私は Andrei Alexandresciu と同じ考えで、セキュリティを確保したいコードのエラーを報告する最もよく知られた方法は例外であると考えています。(プログラマーがエラーリターンコードのチェックを忘れることができないためです。)

アンドレイがそう言ったのを覚えていません - 引用はありますか? 彼は確かに、信頼できる例外処理を促進するオブジェクトを作成する方法について非常に慎重に考えていますが、特定の場合にプログラム停止アサートが不適切であると彼が示唆しているのを聞いたり見たりしたことはありません。アサーションは、不変条件を強制する通常の方法です。どの潜在的なアサーションから継続でき、どのアサーションから継続できないかについて線を引く必要があることは間違いありませんが、その線の片側では、アサーションは引き続き有用です。

エラー値を返すか例外を使用するかの選択は、より正当な代替手段であるため、言及した種類の引数/設定の従来の根拠です。

これが正しければ ... エラー報告に exit(1)/signals/ から例外へのフェーズ変更がある場合 ... 従来のコードとどのように共存していくかという問題がまだ残っています。

上記のように、すべての既存のexit()/ assert などを例外に移行しようとするべきではありません。多くの場合、意味のある処理を続行する方法はなく、例外をスローすると、問題が適切に記録され、意図した終了につながるかどうかについて疑問が生じるだけです。

そして、全体として、いくつかのエラー報告スキームがあります。異なるライブラリが異なるスキームを使用している場合、それらをどのように共存させるか。

それが実際の問題になる場合は、通常、1 つのアプローチを選択し、不適合ライブラリを、好みのエラー処理を提供するレイヤーでラップします。

于 2013-02-13T05:37:36.893 に答える
8

組み込みシステムで実行されるアプリケーションを作成しました。初期の頃、私はコードに assert をふんだんに散りばめ、表向きは不可能なはずのコード内の条件を文書化しました (ただし、いくつかの場所では怠惰なエラー チェックとして)。

アサートがときどきヒットすることが判明しましたが、コンソールのシリアル ポートは通常、どこにも接続されていないため、ファイルと行番号を含むコンソールへのメッセージ出力を誰も見ることができませんでした。後で assert マクロを再定義して、コンソールにメッセージを出力する代わりに、ネットワーク経由でエラー ロガーにメッセージを送信するようにしました。

assert の再定義が「悪」だと思うかどうかに関係なく、これはうまく機能します。

于 2013-02-13T08:26:20.033 に答える
2

を利用するヘッダー/ライブラリを含めると、assert予期しない動作が発生する可能性があります。そうしないと、コンパイラがそれを許可するため、実行できます。

個人的な意見に基づく私の提案は、いずれにしても、既存のアサートを再定義する必要なく、独自のアサートを定義できるということです。新しい名前で新しいものを定義するよりも、既存のものを再定義することで、余分な利点を得ることは決してありません。

于 2013-02-12T20:32:26.217 に答える