こんにちはゲストさん。会員登録(無料)して質問・回答してみよう!

解決済みの質問

オーバーロードしたメンバ関数が継承後,利用できない

オーバーロードしたメンバ関数のクラスを継承後,そのメンバ関数と同名のメンバ関数を定義すると,オーバーロードしたメンバ関数が呼び出せなくなります.何故でしょうか?

質問を一言で正確に表現できてないと思いますので,状況を順を追って説明します.
まず,クラスT1にメンバ関数m(int)があったとします. そのメンバ関数をオーバーロードしてm(float)を作ったとします. そして,T1を継承してT2を定義し,その中でメンバ関数m(int)をオーバーライドすると,T2のオブジェクトではm(float)が呼び出せなくなってしまいました.

何故この様なことをするかといいますと,m(string)はstringの引数をある関数で変換して整数にしてから,intでm(int)を呼び出しているとします.
T2でも,stringによってm(int)を呼び出すので,T1で定義したm(string)をT2でもそのまま使いたいのです.
例です

class T1{
public:
virtual void m(int i){cout<<i<<"T1\n";}
void m(string s){m((int)s[0]);}
};
class T2{
public:
virtual void m(int i){cout<<i<<"T2\n";}
};
int main(){
T2 t;
string s("a");
// t.m(s); /*error*/
t.T1::m(s);
}
出力
97T2
T1のm(string)はT2のm(int)を呼び出してます.

何か解決策ありますでしょうか?
また,言語の仕様がこの様になってるのは,この様なことをすると問題があるからだと思うのですが,どのような問題が起こるので禁止されてるのでしょう?
それとも実は,この様なことはできるのでしょうか?
(つまり,単に私のプログラムが間違ってる)

投稿日時 - 2002-09-27 17:26:56

QNo.367855

暇なときに回答ください

質問者が選んだベストアンサー

まず用語について、申し訳ありません。
確かに。私が間違って覚えていました。(穴があったら、、、)

さて、ご質問の意図は良くわかりましたが、やはり本質的な問題は多重定義した場合は、そのうち一つでもオーバーライドすると全体が隠されてしまうため、T3からT1のメンバ関数に直接アクセスできない(つまりT1のm(const char *)が使えない)ことにありますよね。
結局(多重定義関数は)引数が違うだけでひとまとまりの一つのメンバ関数として扱われているという点です。(そうなっている理由は既に述べたとおりです)

私の思いつく解決策としては、たとえば、

class T1 {
public:
void m(int i) {mm(i);};
void m(char *s){テーブル検索;mm(i);};
protected:
virtual void mm(int i);//mm(int i)にm(i)の処理を移動
};

class T3 : public T1 {
protected:
void mm(int i);
};

と変更する実体の関数を別に用意すれば、そちらはオーバライドされてもかまわないのでこの問題は解決します。
(これがエレガントかといわれると、???ですが、、、T3を作る人にとっては最小限の変更ですみますよね。)
結局のところT1クラスに手を入れない、かつT3で(m(string)を)定義しなおさないというと解決策は無いように思えます。

どんなものでしょうか。

個人的にはT3でT1のm(const char*)を呼び出すm(const char*)を再定義するよりはましかなと思うのですが。
(今回は多重定義一つだから良いけど、10個くらいあるとそのうち一個の変更のために全部は書きたくないですから)

C++からこの制約を取り除こうとすると、暗黙の型変換を禁止しないと出来ないように思えますね。
(なぜいっそのこと暗黙の型変換を禁止にしなかったのかは分からないですね。きっといろんな理由があるんでしょうね。)

実のところ、私はテスト的に色々変更する可能性のあるクラスを作るときはよくこの手を使います。
つまり、インターフェース関数はあくまで顔にして実体の関数はprotectedとかprivateに隠してしまう。
こうすると、後でいろんな変更がしやすいんですよね。たとえオーバヘッドがあっても。

投稿日時 - 2002-09-27 22:26:50

お礼

ご解答ありがとうございます.

お陰様でプログラムの問題点を,ほぼ完全に取り除けました.
これで安心して続きを作成できるというものです.


>確かに。私が間違って覚えていました。(穴があったら、、、)
事の発端は私が質問文を間違えていた事ですから,全く気にすることはないと思いますよ.

投稿日時 - 2002-09-28 01:58:06

このQ&Aは役に立ちましたか?

75人が「このQ&Aが役に立った」と投票しています

回答(5)

ANo.4

言葉が足りなかったので、少し追加します。

>ただ,これだと派生クラスの方で新しく定義したメンバ関数は,呼び出せなくなってしまうという問題がありますね.

その目的の為に、dynamic_cast というのが後から追加されたのですが、
やや無理矢理的な部分を感じていて、私は積極的には使っていません。

>それでも,基底クラスのインターフェイスの設計を頑張れば,解決できるような問題かと思えますので,実用的な解決法と言えますね.

私は、これが C++ の限界と認識しています。
JAVA や C# のような言語では、この問題を一応解決しているのが大きいと思います。

投稿日時 - 2002-09-27 21:18:59

お礼

解答ありがとうございます.

> JAVA や C# のような言語では、この問題を一応解決しているのが大きいと思います。
確かにJAVAによるインターフェイスの設計は,C++よりも簡単かつ簡潔と言える部分が結構ありますね.
JAVAには,C++のSTLと違って(特にVisual C++のやつ),ライブラリもきちんとしたのが揃ってますから魅力的な言語ではありますね.

投稿日時 - 2002-09-27 21:38:01

ANo.3

>最後に1つ疑問に思ったのですが,参照による動的結合は可能だったでしょうか?

わかりやすくする為に、関数を1つあいだにいれますが、

void sub(T1& t){
string s("a");
t.m(s);
}

int main(){
T1 t1;
T2 t2;
sub(t1);
sub(t2);
return 0;
}

こんな感じです。

投稿日時 - 2002-09-27 21:05:22

ANo.2

解決策ですが、
virtual を使う場合は、基底クラスへのポインタ(もしくは参照)を使ってアクセスするしかないと思います。

int main(){
T1 *t1 = new T1;
T1 *t2 = new T2; // ここが重要
string s("a");
t1->m(s);
t2->m(s);
delete t1;
delete t2;
return 0;
}

投稿日時 - 2002-09-27 20:31:39

お礼

回答ありがとうございます.
非常に興味深い回答です.

ただ,これだと派生クラスの方で新しく定義したメンバ関数は,呼び出せなくなってしまうという問題がありますね.
それと,実際にオブジェクトを使う時に,無用なミスを防ぐためにも,質問の様なことを意識しないで呼び出せるようにしたいです.

それでも,基底クラスのインターフェイスの設計を頑張れば,解決できるような問題かと思えますので,実用的な解決法と言えますね.


最後に1つ疑問に思ったのですが,参照による動的結合は可能だったでしょうか?
Javaなどのことを考えると,動的結合のことは混乱してしまいますね.
もし知っておられましたら,再び解答頂けると非常にありがたいです.

投稿日時 - 2002-09-27 20:56:51

ANo.1

ご質問の内容を理解するために確認します。
ご説明とソースが著しく食い違っているので、ご説明のほうを正としますね。
(ソースコード自体動かないはずです)

class T1 {
public:
 virtual void func(int i);
 virtual void func(float a);
};
というように多重定義したわけですね?(これはオーバーロードではありませんよ)
で、派生クラスを、

class T2 : public T1 {
public:
 void func(char *s);
};

と書いたのですね。
これは、class T2 の T2::func(char *s) で基底クラスT1の関数 T1::func()をオーバーロード(今度はオーバーロードです)しています。
つまり、基底クラスのT1::funcは再定義されましたので全部見えなくなります。
つまり、
T2 t;
t.func(1);
t.func(1.0);
は T2::func(char *)を呼び出してしまいます。(コンパイルの型チェックで引っかかると思いますが)
基本的にクラスの親子をまたがって「多重定義」は出来ません。(C++の仕様です。理由はあとで)

解決策はいくつかあります。
1.正攻法
考えてみてください。そもそも派生クラスでそのような同一の働きをする関数を加えることはおかしな話です。
その場合は、普通基底クラスにそのfunc(char *s)を追加すべきなのです。
たとえ基底クラスを変更しても何も問題は起きません。なぜならうまく隠蔽されているからです。

2.邪道1
T2 t;
t.T1::func(1);
t.T1::func(1.0);

はきちんと働きます。(bilboさんのテストがうまくいかなかったのはソースコードに問題があると思いますよ)

3.邪道2
class T2 : public T1 {
public:
 void func(int i) {T1::func(i);};
 void func(float a) {T1::func(a);};
 void func(char *s);
};
と再定義して、T2::func(int i)の中で、T1::func(i)を呼び出せばよい。
ほら、書いてみると分かりますが、これなら初めから基底クラスのT1を変更すべきでしょう。


さて、なぜ派生クラスと基底クラスを含めた多重定義が出来ないのかについて述べますね。
もし、多重定義が出来るとどういうことになるかお話します。

まずA君が基底クラス base を作りました。
class base {
public:
 virtual void func(int i);
};

B君はこれを使って派生クラスderivedを作りました。そして、func2というメンバ関数を作りました。
class derived : public base {
public:
 void func2(double a);
};

さて、A君は色々考えるうちに基底クラスbaseを改良しようと思い、新しい関数を加えることにしました。
そこで、
class base {
public:
 virtual void func(int i);
 virtual void func2(float a);
};
と新しい関数を加えました。もちろんこの関数func2はB君が派生クラスで作った関数func2とは異なる働きをします。

ここで問題が発生します。A君は自分の作ったクラスbaseがB君によってどのように使われているかは分かっていなかったので、B君が作った関数と同じ名前で作ってしまったのです。
もし、A君が
 virtual void func2(double a);
と同じ引数であれば、関数はオーバライドされて問題はなかったでしょう。
しかし、この場合は多重定義になりますから、オーバーライドされなかったわけです。

で、B君のプログラムの中で、

derived foo;
float a;

foo.func2(a);

と呼び出されていたとすると、A君が基底クラスを変更する前であれば、上記プログラムは、
derived::func2(double)
を呼び出していました。(コンパイル時にはワーニングが出ますけどね。エラーではありません。)

しかし、A君が変更した新しいbaseを使うと、なんと!!A君のfunc2を呼び出してしまうではありませんか。
つまり、基底クラスの変更が派生クラスに影響を及ぼしてしまったのです。
こういうことを避けるために、出来ないことになっています。

では。

投稿日時 - 2002-09-27 19:11:27

お礼

丁寧な回答ありがとうございます.
早めに回答戴けてありがたかったです.

質問文が直り切ってなかったのは申し訳なかったですが,プログラムの方も質問文の方も大体は意図は同じです.
ただ,floatとintですと,暗黙の型変換が出来るため,関数の定義がm(float)とm(int)で曖昧になってしまい,コンパイラから文句を言われるので直したのですが,直しきれてなかったと言う経緯があります.

まず,ひとつお尋ねしたいのですが,T2でvoid func(char *s)を定義する事がオーバーロードと言うのでしたら,オーバーライドとは何を意味しているのでしょうか.
演算子のオーバーロードという,重要ですが紛らわしいのがありますから,どっちがどっちだったか分からなくなる事がありますが...
web上に用語解説がありましたので,恐らく多重定義のことをオーバーライドと言うと思うのですが,どうでしょうか?
http://mikata.curiocube.com/oop/terms.html

どうも質問文の意味が曖昧すぎたようですので,もう少し細かく具体的に書きたいと思います.
私が定義していたクラス(クラスT1とします)は,他のクラス(クラスT2とします)を配列で管理する目的のものでした.
クラスT2は名前のメンバを持っているとしまして,それをstringで表現していました.
クラスT1でT2を管理する際に,配列中のどのT2に対して処理を行うかを,配列のインデックスかもしくはT2のオブジェクトの名前で指定したいと考えました.
そこでT1のメンバ関数として,m(int index)と,m(const string &str)を用意することにしました.
m(const string &str)は,対応表からstrに対するindexを引きまして,それをm(int index)に渡すというだけのメンバ関数で,処理の本体はm(int index)の方に定義していました.
ここで,T1からクラスを派生する(クラスT3とします)ことを考えるのですが,T1とT3で異なっているのは,m(int index)の定義だけです.
ですから,名前からindexを引いてm(int index)に渡すだけのm(string &str)は,T3ではヘッダファイルが見辛くなるので一々定義したくないと考えました.
そこで,m(int index)をT1とT3でvirtualにして動的結合してやれば,T1で定義したm(string &str)は,T3のオブジェクトではT3のm(int index)を呼び出してくれると期待しました.

以上が質問の背景です.
そこで再び質問しなおしますが,T3のオブジェクトからT1のm(string &str)を使って,T3のm(int index)を呼び出す方法はないでしょうか?
確かにt3.T1::m(str)としてやれば,問題なく呼び出せますが,他のメンバ関数もありますので,これは使っていて混乱の元です.

これを読んでくれることを切望しながら,失礼させていただきます.

投稿日時 - 2002-09-27 20:47:17