これは、おそらく、C++ プログラマなら誰しも驚いた経験していることであり、 もし、あなたが未経験であってこの文章を読んで知識を仕入れておいたとしても、 きっといつかは体験して驚くことになるであろう、C++ の一仕様の話である。
いささか仰々しい書き出しになったが、今回の話題は、 「基底クラスと派生クラスで同じ名前の関数が定義されていた場合、 基底クラスの関数は隠されてしまう」という、C++ の仕様のことである。
ここで、「同じ名前なら隠されるのは当り前やんか」と思ったあなたは、 ちょっと読みが甘い。「同じ名前」というのは、文字通り、 「関数名だけが一致している」 ということであって、引数など、 シグネチャを構成する他の要素は違っていても構わないのだ。
例をあげよう。
class Base {
public:
int foo( int x );
};
class Derived : public Base {
public:
void foo( double *pd );
};
Derived *pd = new Derived;
pd->foo( 10 ); // エラー!!
『Effective C++』 (p.257) から引用。ただし、関数名は、f から foo に変えてある。
これを見て、「えぇっ !?」 と思ったあなたは、すでに初級の域を脱していて、 C++ でのプログラミングにある程度、自信を持ちはじめているレベルにある方だ。 それでも私は予言しよう。きっとあなたは、いずれ、上のようなコードを書いて、 やっぱり 「えぇっ !?」 と驚くはずだ。この仕様は、クラスの継承や関数の オーバーロードに慣れた者ほど、引っかかりやすいものだからである。
まるでワナのような仕様だが、もちろん、ワナではない。Stroustrup は、 ちゃんとした理由があってこの仕様を導入したのだ。その理由は、 『ARM』 ( The Annotated C++ Reference Manual) にも書いてある (らしい。 ほとんど積読状態なので確認してない)。 たしか、『プログラミング言語 C++ 第3版』にも書いてあった気がするが、 あの本は、やたら膨大な索引が付いているくせに必要なことは なかなか見つけられないという代物なので、探すのはあっさりあきらめて、 やはり、『Effective C++』の説明を借りよう。
まず、Base クラスはあなたが作ったものではなく、しかも、
あなたが Derived クラスを定義するまでに、
いくつものクラス階層を経由していると仮定する。
ようするに、Derived クラスを定義したあなたは、
Base クラスにも 「foo」 という名前のメンバ関数が存在するなんてことは、
全く知らないということだ。
ここで、あなたが、Derived::foo(double *) を呼び出すつもりなのが、
間違って、引数に整数を渡してしまったとしよう。もし、ここで説明したような仕様が
導入されていなかったら、コンパイラは何の疑いをはさむことなく、
Base::foo(int) を呼び出すようなコードを生成してくれるだろう。
そしてあなたは、意味不明のバグに 三日三晩は悩まされる、というわけだ。
つまり、プログラマにちょっとした不便さを強いるかわりに、
より安全な側に仕様を倒しているわけである。まあ、不便といっても、
Base::foo() を呼び出したければ、次のようにすればよいので、
さしたる問題ではなかろう。
。
class Derived : public Base {
public:
using Base::foo;
void foo( double *pd );
};
「問題はない」と言ったが、手元の g++ (2.95.2) では、上のコードがうまく コンパイルできない。VC++ 6.0 ではちゃんとコンパイルできるのに。
2001-07-05 追記: 最近リリースされた g++ 3.0 では、上のコードも通るようだ。 Improved C++ Support in GCC 3.0 というドキュメントに、 "G++ now supports importing member functions from base classes with a using-declaration." と書かれている。
しかし、この仕様は、あくまで、 Derived クラスのメンバ関数を直接 呼び出そうとしている場合にしか有効ではないのだ。次のように、 関数を仮想化して間接的に呼んでやると…
class Base {
public:
virtual int foo( int x );
};
class Derived : public Base {
public:
virtual void foo( double *pd );
};
Base *pb = new Derived;
pb->foo( 10 ); // 成功
コンパイルに成功して、Base::foo が呼び出されてしまう。
仮想関数の目的および仕組みから言えば当然の帰結なのだが、
本稿の話題である ワナ 仕様との不整合があるような気がして
どうにも釈然としないのは、
やはり私も、まだまだ未熟者であるということかしらん。
初出: 2001年6月16日