例外コンテキストのロギング

プログラマーの日を見越してテスターの日をきっかけに、ソフトウェアの開発とデバッグという共通のビジネスの両方の生活を簡素化する方法についてお話したいと思います。

つまり、C ++例外の情報量を増やし、エラーログをよりコンパクトにする方法について説明します。



Javaでの作業の1年半後、StackTraceのような例外に慣れました

スレッド「メイン」の例外java.lang.IllegalStateException:ブックにnullプロパティがあります
         com.example.myproject.Author.getBookIds(Author.java:38)で
         com.example.myproject.Bootstrap.main(Bootstrap.java:14)で
原因:java.lang.NullPointerException
         com.example.myproject.Book.getId(Book.java:22)で
         com.example.myproject.Author.getBookIds(Author.java:35)で
         ...その他1


C ++に切り替えた後、しばらくして、エラーの原因を特定し、さらにデバッグするという点で、情報のない例外の性質に感銘を受けました。 正確に何かがうまくいかず、例外が発生した場所をログから理解するために、プログラムのすべての重要なポイントの経過を記録する必要がありました。

次に、私が経験したロギングの単純化された進化を紹介します。

デバッグおよび改善する元のプログラム
void foo(int a) { if (a == 0) { throw runtime_error("foo throw because zero argument"); } } void bar(int a) { foo(a - 10); } int main() { try { for (int i = 0; i < 100; i++) { bar(i); } } catch (const exception &e) { cerr << "Caught exception: " << e.what() << endl; } return 0; }
      
      







この形式では、 fooの呼び出しパスとbar関数について何も知らずに、受け取った例外をどう処理するかを理解することは非常に困難です。

キャッチされた例外:引数がゼロであるためfoo throw


少しログを追加する
 void bar(int a) { cerr << "Calling foo(" << a - 10 << ")" << endl; foo(a - 10); } int main() { try { for (int i = 0; i < 100; i++) { cerr << "Calling bar(" << i << ")" << endl; bar(i); } } catch (const exception &e) { cerr << "Caught exception: " << e.what() << endl; } return 0; }
      
      







実行結果:
通話バー(0)

fooの呼び出し(-10)

通話バー(1)

fooの呼び出し(-9)

通話バー(2)

fooの呼び出し(-8)

通話バー(3)

fooの呼び出し(-7)

通話バー(4)

fooの呼び出し(-6)

通話バー(5)

fooの呼び出し(-5)

通話バー(6)

fooの呼び出し(-4)

通話バー(7)

fooの呼び出し(-3)

通話バー(8)

fooの呼び出し(-2)

通話バー(9)

fooの呼び出し(-1)

通話バー(10)

fooの呼び出し(0)

キャッチされた例外:引数がゼロであるためfoo throw



これで何が起こったのかは明確になりましたが、ログは散らかっていました。 また、プログラムがディレクトリ内のすべてのファイルを処理する必要がある悪夢を想像してください。ログの5行が各ファイルに該当し、1000ファイルの後に例外が発生しました。 合計-すべてが正常であるかどうかに関するログの5000行、およびエラーログの10行。

このログを読んでいる間、休日の代わりに私のログを読んでバグレポートを書く仲間の開発者とテスターの呪いは、私のカルマをマイナスの無限に追いやります。

したがって、「誤った」実行ブランチのみを記録する必要があります。

関数が呼び出された時点で、関数が正常に終了するか例外をスローするかは不明です。 そのため、関数を終了してプログラムの進行状況を分析するまで、ログ記録を延期する必要があります。

そのように
 void bar(int a) { try { foo(a - 10); } catch (const exception &e) { string msg = string("calling foo(") + to_string(a - 10) + ") failed"; throw runtime_error(string(e.what()) + "\n" + msg); } } int main() { try { int i; try { for (i = 0; i < 100; i++) { bar(i); } } catch (const exception &e) { string msg = string("calling bar(") + to_string(i) + ") failed"; throw runtime_error(string(e.what()) + "\n" + msg); } } catch (const exception &e) { cerr << "Caught exception: " << e.what() << endl; } return 0; }
      
      





キャッチされた例外:引数がゼロであるためfoo throw

foo(0)の呼び出しに失敗しました

呼び出しバー(10)が失敗しました



テスターはバグレポートを簡単に作成し、美しく、有益でクリーンなログを添付できます。 しかし、プログラムはくなり、例外の魅力をすべて失いました-動作中のコードを壊す機能とエラー処理。 実際、関数の戻りコードと純粋なCの残酷な時間のチェックにほとんど戻りました。しかし、プログラムを変形させずに、きれいなログを作成するために使用できる美しいソリューションが必要です。 つまり 誰かが機能を離れるときに私たちのために何が起こっているかを分析し、誓約する必要があります。 ここで、Habrですでに説明したアプローチ、つまりデストラクタの呼び出しでのログの記録が私たちを救います。



したがって、ロギングクラスの要件は次のとおりです。

  1. ログのメッセージを設定する
  2. メッセージ出力、例外の場合のみ


ここでは、 bool uncaught_exception()関数が役立ちます。これは、未処理の例外があるかどうかを示すだけです。

クラスExceptionContext
 class ExceptionContext { public: ExceptionContext(const std::string &msg); ~ExceptionContext(); private: std::string message; }; ExceptionContext::ExceptionContext(const std::string &msg): message(msg) {} ExceptionContext::~ExceptionContext() { if (std::uncaught_exception()) { std::cerr << message << std::endl; } }
      
      







使用例
 void bar(int a) { ExceptionContext e(string("calling foo(") + to_string(a - 10) + ")"); foo(a - 10); } int main() { try { for (int i = 0; i < 100; i++) { ExceptionContext e(string("calling bar(") + to_string(i) + ")"); bar(i); } } catch (const exception &e) { cerr << "Caught exception: " << e.what() << endl;http://habrahabr.ru/topic/edit/266729/# } return 0; }
      
      





fooの呼び出し(0)

コーリングバー(10)

キャッチされた例外:引数がゼロであるためfoo throw


このオプションは、ログのコンパクト性(実行のフォールダウンブランチのみをログに記録する)とプログラムのコンパクト性を組み合わせていることがわかります(メインコードと例外処理は間隔を空けており、ログはメインコードに1行で挿入されます)。 現在、開発者とテスターの両方が私をのろいをやめています。

実際、主な目標はすでに達成されていますが、すでに述べた投稿の最後に記載されているものを含む、多くのさらなる改善の道をたどることができます。

次の2つの点のみを検討します。

  1. 他のロガーとの相互作用
  2. スレッドセーフ


cerrでログを直接印刷することは、蓄積されたコンテキストを他のどこかに複製することが困難なため(少なくとも、開発者へのメールで、ボリュームを大幅に削減したため)取得する機能がないため、不便です。 繰り返しますが、他のログツールまたはマルチスレッド実行の存在下では、ログ行が混ざったような不快な特殊効果が発生する可能性があります。 したがって、 ExceptionContextクラスは、JavaからのprintStackTraceの方法で、ログを内部に保存し、要求に応じて外部に発行します。



スレッドセーフオプション(C ++ 11を使用)
 class ExceptionContext { public: ExceptionContext(const std::string &msg); ~ExceptionContext(); static void Print(); //!    cerr   . private: std::string message; static thread_local std::string global_context; //!      . }; ExceptionContext::ExceptionContext(const std::string &msg): message(msg) {} ExceptionContext::~ExceptionContext() { if (std::uncaught_exception()) { global_context += message + std::string("\n"); } } void ExceptionContext::Print() { std::cerr << global_context << std::endl; global_context.clear(); } thread_local std::string ExceptionContext::global_context = std::string("");
      
      







メインプログラムのcatchブロックは、次のようになります。

  catch (const exception &e) { cerr << "Caught exception: " << e.what() << endl; ExceptionContext::Print(); }
      
      







C ++ 11以降では、 thread_local修飾子が使用されます。これは、クラスの静的メンバーであり、すべてのインスタンスで同じであるという事実にもかかわらず、実行の各スレッドでglobal_contextオブジェクトが独自のものになることを保証します。



良い週末、きれいなコード、読み取り可能なログ、その他の成功をお楽しみください!



All Articles