ベースクラスの関数シグネチャを不用意に変えてはいけない

会社で実際にあったバグ。とりあえずこんなふうに継承関係のあるクラスがありました。

class CBaseClass {
public:
	CBaseClass() { }
	~CBaseClass() { }

	virtual void TestFunc() {
		std::cout << "CBaseClass::TestFunc() called.\n";
	}
};

class CDerivedClass : public CBaseClass {
public:
	CDerivedClass() { }
	~CDerivedClass() { }

	virtual void TestFunc() {
		std::cout << "CDerivedClass::TestFunc() called.\n";
	}
};

ほとんど同じなんだけど、最後のステップだけ違う処理をする2つのクラス。違う処理をするメソッドだけ、仮想関数にして差し替えることで、実装を共通化してありました。そして呼び出し側では、

...
	CBaseClass baseObj;
	CDerivedClass derivedObj;
	CBaseClass* pBase;
...
	if (some condition) {
		pBase = &baseObj;
	}
	else {
		pBase = &derivedObj;
	}
...
...
	pBase->TestFunc();

こんなふうに、最初の条件チェックでどちらかのクラスのオブジェクトへのポインタを作成しておいて、共通の処理を実行、最後に仮想関数呼び出しで適切な後処理をする、というコードです。

ところが、いつのバージョンからか、2つのうち最初のクラスの後処理だけ、さらに条件分岐する処理が追加されていました。

class CBaseClass {
public:
	CBaseClass() { }
	~CBaseClass() { }

	virtual void TestFunc(int param = 0) {  //引数'param'を追加(でも元と同じ引数なしでも使う)
		std::cout << "CBaseClass::TestFunc(" << param << ") called.\n";
	}
};

デフォルト引数を与えてあるので、コードのほかの部分には影響ないよねー、ということだったと思うのですが、どうにも変な動きをします。デバッガで追ってみると、「2つのクラスで違う処理をする」TestFunc()が、必ずCBaseClass::TestFunc()に分岐してしまっています。

	pBase->TestFunc(); //必ずCBaseClass::TestFunc()が呼ばれるよ!

結局、CBaseClass::TestFunc()に引数を加えたことで、TestFunc()のメソッドシグネチャが変わってしまい、CDerivedClass::TestFunc()がCBaseClass::TestFunc()をオーバーライドしなくなってしまっていたのでした。

void CBaseClass::TestFunc(int param = 0); //この2つは
void CDrivedClass::TestFunc();            //まったく別のメソッド

それで、冒頭のベースクラスの関数シグネチャを不用意に変えてはいけないになるのですが...
この「デフォルト引数を除くと同じ名前のメソッドが、継承関係のあるクラスでそれぞれvirtual宣言されている」という状況、C++の文法的には全く問題がないのですね。
VC++で警告レベルを/W4にしても、何の警告も出ません(ためしにgccで-Wallとしてもやってみましたが同じでした)。

まあ、単純ミスといえば単純ミスなんですが、C#のvirtual/overrideキーワードをそれぞれ指定する文法だとエラー検出できるよね、とか、そもそもCBaseClassとCDerivedClassから共通の抽象クラスを抽出する設計にすべきだったんじゃ、とかいろいろ考えてしまいました。

でも今回のケースでは、(このコードが実装しようとしていた)元の仕様が無駄に複雑すぎるのが一番の問題、というオチだったりして。