エラーコードには開発者向け情報を埋めよう

2020年1月18日

ソフトウェア製品であれば何であれ、エラーコードと呼ばれるものがありますね。"404 Not Found" は、世界で最も有名なエラーコードの一つです。

組み込み製品の場合は、表示部がせいぜい数桁のナナセグしかないようなことも常ですから、この領域で何とかユーザーに物事を伝えようと考えると、なおさらエラーコードが重要になってきます。

ここで、私は声を大にして言いたい。あなたがプログラマーなら、外部仕様がどうだろうと、「あなたの」必要に応じて、エラーコードを工夫すべきだと。

ユニークでなければエラーコードではない

あなたが作っている製品のエラーコード体系は、そこから辿ればソースコードの「どのファイルの」「何行目で」発生しているか、特定できるものになっていますか? つまりユニークですか。

すでにそうなら、ここで述べることは何もありません。ですがおそらく、そうではないでしょう。それには理由があって、仕様を定義する立場から見れば、あるエラーがソースコード上どのようなロジックで実現されるのか、知りようがないからです。ですからプログラマーは、外部仕様は満足しながらも、独自の工夫をエラーコード体系に盛り込まなければなりません。

繰り返しますが、例外なくすべてのエラーが「どのファイルの」「何行目で」発せられたか特定できる仕掛けを入れてください。それで得られる莫大な対価に比べれば、そのためのコストはタダみたいなものです。

具体案

ここからは、ユニークなエラーコード体系を実現するための具体的な方策を提案します。ここでは前提として、エラーを通知するために次のようなコードがアプリケーション全体に埋め込まれているシステムを想定して、話を進めます。

void checkTemperature()
{
  if (g_celsius < -25.0 || 90.0 < g_celcius)
  {
    g_event.notify(0x005A);  // 005A: 温度異常
  }
}

提案 1:サブコードを付加する

外部仕様で定められたエラーコード体系だけでは重複が生じてしまう場合、開発者独自のサブコードを設けて重複を回避しましょう。たとえば次のように。

void checkTemperature()
{
  if (g_celsius < -25.0 || 90.0 < g_celcius)
  {
    g_event.notify(0x005A, 0x01);  // 005A.01: 温度異常
  }
}

すでに外部仕様にサブコードがあるなら、サブサブコードを付加してください。重要なのは、「すべての」エラーにこれを設けるという点です。エラー通知の仕組みをアプリケーション全体で統一すれば、その点は問題ないでしょう。

外部仕様を逸脱しているのでは、などと思う必要はありません。このサブコードをユーザーに公開しなければ、本来の仕様を満足することは明らかです。最悪、リリース時に無効化することもできます。その判断は後に回して、入れられる工夫を先に入れましょう。

また、サブコードを活かして、検出のロジックはできるだけ「刻む」ほうが賢明です。たとえ外部仕様が「低温または高温で」同じ温度異常だと言っていても、コードは次のように刻んでおくのです。

void checkTemperature()
{
  if (g_celsius < -25.0)
  {
    g_event.notify(0x005A, 0x01);  // 005A.01: 温度異常
  }
  else if (g_celcius > 90.0)
  {
    g_event.notify(0x005A, 0x02);  // 005A.02: 温度異常
  }
}

このように無理やりにでも重複を回避することは、開発の後半、そして出荷後に、じわじわと効いてきます。

提案 2:マクロを利用する

サブコード方式の欠点は、サブコードが本当にばらけていること、つまりユニークであることを保証するのが難しい点にあります。開発規模によっては、管理者を立てて文書で管理する羽目になりかねず、それだけは避けたいところです。

C には標準で __FILE__ および __LINE__ というラベルが定義されています。これは、このラベルを置いた場所そのものを示すという優れもので、どんな開発環境にも備わっています。例のコードに適用すると、このような感じになります。

void checkTemperature()
{
  if (g_celsius < -25.0)
  {
    g_event.notify(0x005A, __FILE__, __LINE__);  // 005A: 温度異常
  }
  else if (g_celcius > 90.0)
  {
    g_event.notify(0x005A, __FILE__, __LINE__);  // 005A: 温度異常
  }
}

これで関数の呼び出し先は、呼び出し元のファイル名と行番号を受け取ることができます。簡単にエラーコードの唯一性が保証できる、素晴らしい方法です。

ただし、少しだけ注意点があります。上記のコードを見て、毎回同じようなコードを書くのは嫌だと思うのは、プログラマーとして正しい感覚です。ですがこのコードは、次のような単純な共通化はできないのです。

class Event
{
public:
  void notify(unsigned int code)
  {
    /* __FILE__ , __LINE__ を使って何かやる */
  }
};

Event g_event;

void checkTemperature()
{
  if (g_celsius < -25.0)
  {
    g_event.notify(0x005A);  // 005A: 温度異常
  }
  else if (g_celcius > 90.0)
  {
    g_event.notify(0x005A);  // 005A: 温度異常
  }
}

これをやってしまうと、ラベルの性質上、結局すべてのエラーで同じファイル名・行番号が刻まれてしまうからです。この問題を回避するには、マクロを使って次のようにしなければなりません。

class Event
{
public:
  void notify(unsigned int code, const char *file, int line)
  {
    /* file, line を使って何かやる */
  }
};

#define notify(code) notify(code, __FILE__, __LINE__)

Event g_event;

void checkTemperature()
{
  if (g_celsius < -25.0)
  {
    g_event.notify(0x005A);  // 005A: 温度異常
  }
  else if (g_celcius > 90.0)
  {
    g_event.notify(0x005A);  // 005A: 温度異常
  }
}

C++ の世界ではマクロは基本的に悪ですが、これに関しては他の手段がありません。例外的措置とお考えください。

提案 3:エラーメッセージを添える

すべてのエラーに、ユニークなメッセージを添えるという方法もあります。開発者を助けるという意味では、これが最も優れた方法です。

void checkTemperature()
{
  if (g_celsius < -25.0)
  {
    g_event.notify(0x005A, "Too low temperature detected!");
  }
  else if (g_celcius > 90.0)
  {
    g_event.notify(0x005A, "Too high temperature detected!");
  }
}

しかしこの方法にも欠点があり、役に立つメッセージを(しかも重複のないように)考え抜くのが面倒くさいという点です。気を抜くとすぐに誰かがコピペで誤情報を量産するような事態に陥るでしょう。反対に、このシステムに込められた意図がチーム全体で共有されているなら、これほど強力なものはありません。

まとめ

多くの開発経験を踏んだ人ならおわかりかと思いますが、製品が発するエラーというのは、巡り巡ってプログラマーに返ってきます。それは質問や苦情といった形で、プログラマーの時間を奪っていきます。

このとき、電話一本で、あるいはメール一つでエラーコードを尋ねれば、何が起こっているかわかる。このアドバンテージは非常にデカいです。ことによっては、飛行機のチケット往復分が浮く場合だってあるのですから。