空想犬猫記

※当日記では、犬も猫も空想も扱っておりません。(旧・エト記)

if のスコープの話

C++を生業にしてかれこれ8年くらい。C++を現実の問題を解くための単なるツールという本来の役割として活用はしているものの、ここ数年あまりC++の言語仕様については深く調べていない。私を含めて今のチームのメンバーはみんな現場のオッチャンプログラマなのだ。久々に、ドキッとするバグをみたのでメモしておこう。

とあるフレームワークでは、C++でObserverパターンを実装している。Observableの派生クラスから任意のイベントオブジェクトを通知し、Observerは飛んできたイベントのタイプに応じて、自分自身に必要な処理をする。ここでは詳細には踏み込まないが、そのフレームワークではイベントのタイプを識別するのに、C++のオブジェクトシステムが提供するRTTI実行時型情報を利用している。具体的には、Observerが抽象的なイベント型を引数にとる仮想関数を持っていて、実際のObserverの導出クラスの実装でdynamic_castを使ってイベントのタイプを判別する。イベントの種類をC++のタイプにマップしているのである。

イベントハンドリングの仕組みは、大体こんな感じ。

#include <iostream>

using namespace std;

struct Event { virtual ~Event(){} };
struct OnEventA : public Event { int Data() { return 10; } };
struct OnEventB : public Event { int Data() { return 20; } };

static void HandleEvent(Event* event)
{
  if (OnEventA* ev = dynamic_cast<OnEventA*>(event))
  {
    cout << "OnEventA::Data() = " << ev->Data() << endl; // OnEventA を処理
  }
  else if (OnEventB* ev = dynamic_cast<OnEventB*>(event))
  {
    cout << "OnEventB::Data() = " << ev->Data() << endl; // OnEventB を処理
  }
}

int main()
{
  OnEventA eventA;
  OnEventB eventB;

  HandleEvent(&eventA);
  HandleEvent(&eventB);

  return 0;
}

これを実行すると

OnEventA::Data() = 10
OnEventB::Data() = 20

このコードは意図したとおりに動いている。

で今日レビューで発見したのは、以下のようなコード。

static void HandleEvent(Event* event)
{
  if (OnEventA* ev = dynamic_cast<OnEventA*>(event))
  {
    cout << "OnEventA::Data() = " << ev->Data() << endl; // OnEventA を処理
  }
  else if (OnEventB* ew = dynamic_cast<OnEventB*>(event)) // タイポ!
  {
    cout << "OnEventB::Data() = " << ev->Data() << endl; // OnEventB を処理
  }
}

知らなかったけど、これビルドが通ってしまう。OnEventBが飛んできたときの挙動が圧巻で、if の直後のdynamic_castは失敗し、evはNULLで初期化される。次にewのdynamic_castが成功し、else if節に入り、ev->Data() が呼ばれるのだが、タイポのせいでelse if節の外側で初期化されたev(== NULL)に対するメンバ関数呼び出しが呼ばれ、

OnEventA::Data() = 10
OnEventB::Data() = 10

という結果になる(仮想関数呼び出しではないのでヌルポインタに対してメンバ関数が呼べる)。。。

ってことは恐らくつまり、

if (A* a = dynamic_cast<A*>(ptr)) {
  // :
}
else {
  // :
}

というのは

{
  A* a = dynamic_cast<A*>(ptr)
  if (a) {
    // :
  }
  else {
    // :
  }
}

と等価ということなのかねェと気付いたしだい(オッチャンプログラマは原典は当たらない)。

何とかこれは、先輩によるコードレビューの段階で気付くことができて、コミット未遂で済んだけど、何年使っても未だに落っこちることのできる落とし穴があるとは。

奥ゆかしきかなC++

練習問題

static void HandleEvent(Event* event)
{
  if (OnEventA* ev = dynamic_cast<OnEventA*>(event))
  {
    cout << "OnEventA::Data() = " << ev->Data() << endl;
  }
  else if (OnEventB* ev = dynamic_cast<OnEventB*>(event))
  {
    cout << "OnEventB::Data() = " << ev->Data() << endl;
  }
  else
  {
    cout << typeid(ev).name() << endl;
  }
}

この関数にNULLポインタが渡されたときに、elseで表示されるポインタのタイプは何?