オーバーロードとオーバーライド
ところで、オーバーロードとオーバーライド、この2つの違いを説明できるでしょうか。分かっていても、どちらがどちらか混乱してしまう人も少なくないのではないかと思います。念のため整理すると、
- オーバーロード:
- オーバーライド:
となります。さて、次はオーバーロードとオーバーライドに関するケースです。まず、オーバーロードについて簡単な例をご覧ください。
#include <stdio.h> class Parm { public: void print(double x) { printf("%f\n", x); } void print(char *x) { printf("%s\n", x); } }; int main(void) { Parm parm; parm.print(2.3); /* A */ parm.print("2.3"); /* C */ return 0; }
2.300000 2.3
実行結果から分かるように、コンパイラは引数の型を判断し、Parm
クラスのどちらのメンバ関数を呼び出すかを決定します。
次にオーバーライドの簡単な例を示します。
#include <stdio.h> class Parm { public: virtual void parm(int x){ printf("parm=%d\n", x); } }; class SubParm : public Parm { public: void parm(int x){ printf("Ah, my!"); } //引数とは関係ない文字列を表示 }; int main(void) { Parm *sp = new SubParm; sp->parm(2); //SubParm::parm関数が呼び出される delete sp; }
Ah, my!
このように、コンパイラはsp
の実体がどの型で作られているかを判断して、呼び出すメンバ関数を決定します。
ところで、オーバーロードとオーバーライドが同時に利用される場合はどうでしょうか。以下の例をご覧ください。
class Parm { public: virtual void parm(int); // A virtual void parm(double); // B }; class SubParm : public Parm { public: void parm(int); // C }; //// //// 各メンバ関数の実装は省略 //// int main(void) { SubParm *sub = new SubParm; Parm *sp = sub; sp->parm(2.3); // D sub->parm(2.3); // E }
このようなクラス構成では、メンバ関数のオーバーロードとオーバーライドが同時利用されることになってしまいます。main
関数のコメントDではコメントBの関数が呼ばれますが、コメントEではオーバーロードによって基底クラスの仮想関数を隠匿してしまうため、コメントCの関数が呼ばれ、double
型の引数はint
型の引数として渡されてしまいます。この動作は作成者の意図に反したものなのではないかと想像できますが、もし作成者が意図したものだとすると、このソースコードを読む人を混乱させる原因になるでしょう。
この問題を解決するには、オーバーロードされた全ての仮想関数を派生クラスでオーバーライドさせたり、非仮想関数をオーバーロードして関数名の異なる仮想関数を呼び出すようにすれば良いでしょう。しかし、大事なのはこのような解決方法ではなく、オーバーロードとオーバーライドについてきちんと理解し、上記のような問題に陥らないようにすることであることは言うまでもありません。
まとめ
いくつかの例を挙げてC++の罠について述べてきましたが、いかがだったでしょうか。冒頭でも述べた通り、通常業務でC++を使用しているエンジニアの方の中には、退屈なものだと感じた方もいらっしゃるかもしれません。しかし、そのようなエンジニアの方こそ、ここで挙げたような例に頭を抱えた経験があるのではないでしょうか。現実問題として、言語仕様の基礎を身につけずに目の前の開発に追われ続けてしまうプログラマは少なくありません。もちろん、ソースコードレビュー、インスペクション、ペアプログラミングなどを駆使してこのような不具合を潰していくような活動は必要ですが、その前段階、つまりエンジニア個人の能力を高めていく努力が不可欠であり、その王道は基礎をしっかり身に付けることに違いありません。この記事が、読者の皆様に基礎の重要性を再認識していただくきっかけとなることを願って止みません。