ページ 11

仮想関数について

Posted: 2010年9月25日(土) 20:51
by シエル
いつもお世話になります。

今回は仮想関数について質問させてください。

まず下記のコードをご確認ください。
クラスBASEを継承した、クラスA、Bがあります。
class BASE{
public:
    int x,y,z;
    virtual void Calc(){
        z=x+y;
    }
};

class A : public BASE{
    int x,y,z;
public:
    void Calc(){
        z=x-y;
    }
};

class B : public BASE{
    int x,y,z;
public:
    void Calc(){
        z=x*y;
    }
};
そこで質問があります。

質問①
基底クラスのCalc関数を仮想関数にしていますが、その派生クラスである、A、Bのクラスでは
Calc関数をオーバーライドしています。
このように派生クラス側でオーバーライドする場合、BASEクラスを継承し、Calc関数を仮想関数化する意味
はあるのでしょうか?
自分の考えでは、基底クラスのポインタを使って、各派生クラスのインスタンスを作成し、
そのポインタを使って、それぞれのクラスのCalc関数が呼ばれるようにするぐらいしかメリットはないですよね?
つまり、各派生クラスでA a,B bのようにそれぞれでインスタンスを作ってしまえば、
仮想関数にする意味はないのではないかと思っているんです。

質問②
インターフェースクラスについて。
BASEクラスを継承したクラスA、Bのインスタンスでしか、Calc関数は呼び出さないとします。
つまり、BASE baseみたいに基底クラス単体でインスタンス作って、その関数を呼び出さないってことです。
その場合、基底クラスの仮想関数をvirtual void Calc() = 0として、純粋仮想関数化し、
インターフェースクラスにするべきなのでしょうか?

質問③
インターフェースクラスにした場合、「インタフェースクラスには仮想デストラクタを宣言しておくべき」と
このhttp://www.geocities.jp/ky_webid/cpp/language/010.htmlの下のほうに
書いてありますが、この理由を教えてください。
また、そのインターフェースクラスを継承した派生クラスにも仮想デストラクタを宣言する必要があるのか、
教えてください。

質問④
基底クラスの関数にvirtualをつけて、仮想関数化した場合、その派生クラスには該当の関数にvirtualを
つける必要はありますでしょうか?
いらないと思ったんですが、このhttp://marupeke296.com/GDEV_No7_StateImpliment2.htmlに、派生クラス側にもvirtualをつけてるものがあったものでして^^;


大体以上です。分かりにくくて申し訳ありませんが、よろしくお願い致します。

Re:仮想関数について

Posted: 2010年9月25日(土) 21:31
by 組木紙織
1:
私は、質問の意味や意図が分からないので回答できません。

2:
デフォルトの実装が必要な場合には仮想関数でよい。
デフォルトの実装が必要でない場合には純粋仮想関数にすべき。
と考えています。
純粋仮想関数が一つでもはいっていたら仮想クラスとなります。
したがって、ほかに仮想関数がいくら入っていてもインスタンス化できないクラスとなります。
(質問の意味を十分把握できていないので、的外れかも知れませんが。)


3:
仮想デストラクタは必要です。
"インターフェースクラス"じゃなくても、"ポリモーフィズムとして扱うように考えているクラス"は必要です。
サンプルコードを書いて見ればすぐわかります。

4:
さらに継承をすることを考えているなら必要。そうでないなら必要なし。


#サンプルコードはどのような使われ方をするか知りませんが、良くないコードだと思っています。

Re:仮想関数について

Posted: 2010年9月25日(土) 21:53
by array
> 質問① に関してです。

基本的に仮想関数にかんしては、クラスに柔軟性を持たせる場合に使います。(私の場合は)


class BASE {
public:
int x,y,z;
BASE() { x = y = z = 5; }; // 5で初期化
virtual int Calc() = 0;
};

class A : public BASE {
public:
int Calc() {
z = x - y;
return z;
}
};

class B : public BASE {
public:
int Calc() {
z = x * y;
return z;
}
};


int main() {
#if 1
BASE c = new A();
#else
BASE c = new B();
#endif

std::cout << c->Calc() << std::endl; // 計算結果を出力

delete c;

return 0;
}

#ifに0を指定すればコンパイル時に動作が変わります(動作未確認)。


例ではインパクトが弱いですが、同じ用法を使って私の場合はDirectXの9と11の動作切換えに使えて便利だと思っています。

Re:仮想関数について

Posted: 2010年9月25日(土) 21:59
by dic
1.
基本クラスで仮想関数を宣言して
派生クラスで仮想関数の実態を定義します
ポリモーフィズムの利点は分かっているようですが、実践では使ったことがないのではないでしょうか?
#include <iostream>
using namespace std;

class    Base
{
public:
    virtual    void    Init();
};
class    A    :    public    Base
{
public:
    void    Init();
};
class    B    :    public    Base
{
public:
    void    Init();
};
//    新しいクラス
class    C    :    public    Base
{
public:
    void    Init();
    void    Original();
};
void    Base::Init(){}
void    A::Init(){}
void    B::Init(){}
void    C::Init(){}
void    C::Original(){}

int main()
{
    Base    *a, *b;
    a = new A;
    b = new B;
    a->Init();
    b->Init();


    //    新しく追加仮想関数があるので
    //    Init関数にアクセスできる
    Base    *c;
    c = new C;
    c->Init();

    //    Originalにはアクセスできない
    //    x c->Original();

    //    でははじめからこうすると
    C    *c1 = new C;
    c1->Original();

    return 0;
}
これでは基本クラスを継承した意味がなくなりますし
まったく基本クラスと派生クラスには関係がなくてもいいので継承を使う意味がないです
「仮想関数は基本的に派生クラスで実装する」という独自の作法を本から学びました



2.
純粋仮想関数を持つクラスはインスタンス化できません

>つまり、BASE baseみたいに基底クラス単体でインスタンス作って、その関数を呼び出さないってことです。
ちょっとここがよくわからないですね


3.4については 組木紙織さんと同じ意見です

Re:仮想関数について

Posted: 2010年9月25日(土) 22:05
by array
追記補足です。

上記の例は、あくまで仮想関数にメリットなどの意味を見出すならという見解の解答です。

よってBASEクラスでは特にCalc()の内容を定義する必要性を感じなかったので純粋仮想関数にして
派生クラスでは計算結果を返すようにと、ある程度意味のあるクラスにしてみました。

注目してほしいのは、計算結果の出力が違う点で、そこにクラスの柔軟性が垣間見れると思います。

そこに 組木紙織さんの言うようにデフォルトの動作が必要な場合は仮想関数として実装すれば良いと思います。

Re:仮想関数について

Posted: 2010年9月25日(土) 22:13
by MNS

多態性を実現するなら必要でしょう。
>基底クラスのポインタを使って、各派生クラスのインスタンスを作成し、
>そのポインタを使って、それぞれのクラスのCalc関数が呼ばれるようにする
まさにこれです。
多態性を使わないと、継承をする意味があまりありません。


確か、不要だった気がします。
(その派生クラスがさらに継承されるとしても)
ですが、付けても付けなくても変わりませんから、
明示するに越したことはないでしょう。
画像

Re:仮想関数について

Posted: 2010年9月25日(土) 22:22
by gyz
私も勉強中の身ですので100%正しいとは言い切れませんが
回答させていただきます
-------------------------------------------
質問①
>派生クラス側でオーバーライドする場合、BASEクラスを継承し、Calc関数を仮想関数化する意味はあるのでしょうか?

派生クラスを継承するクラスを作る予定が無いのなら、厳密には派生クラスのメソッドにvirtualつける必要は無いです
ただ、規約や、オーバーライドしているという分かりやすさを考慮して派生側にもvirtualを付ける場合が多いようです。
仕様変更があって、継承する必要が出てしまうかもしれませんしね。

>基底クラスのポインタを使って、各派生クラスのインスタンスを作成し、
>そのポインタを使って、それぞれのクラスのCalc関数が呼ばれるようにするぐらいしかメリットはないですよね?

ぐらいしかメリットが無いというのは間違ってると思います
仮想関数のもっとも大きなメリットでは無いでしょうか?
多態性はコレなしでは実現出来ないでしょう
-------------------------------------------
質問②
>基底クラス単体でインスタンス作って、その関数を呼び出さない

この場合は純粋仮想関数にして、仮想クラス化するべきだと思います
-------------------------------------------
質問③.
>インターフェースクラスにした場合、「インタフェースクラスには仮想デストラクタを宣言しておくべき」と
>このページの下のほうに
>書いてありますが、この理由を教えてください。

質問①の仮想関数と同じ理由です
仮想デストラクタにしなかった場合、
基底クラスのポインタに派生クラスのインスタンスを格納した場合、基底クラスのデストラクタしか実行されなくなるからです。

>また、そのインターフェースクラスを継承した派生クラスにも仮想デストラクタを宣言する必要があるのか、
>教えてください。

これも質問①の仮想関数と同じです
派生クラスを継承したクラスを使用予定がない場合は、厳密には派生クラスのデストラクタにvirtualは必要ありません

こちら
http://www5c.biglobe.ne.jp/~ecb/cpp/06_09.html
のページの右側のコードの、子クラスのデストラクタのvirtualを消して
実行してみてください、ちゃんと子クラスのデストラクタも動きます。
-------------------------------------------
質問④
質問①の回答と同じ

Re:仮想関数について

Posted: 2010年9月25日(土) 22:46
by gyz
MNSさんの回答を読んで、
子クラスから派生した孫クラスを作成して試したところ、
親クラスのデストラクタにvirtualがついていれば
子クラスと孫クラスのデストラクタにvirtualが無くても
親クラスのポインタに、子、孫クラスのインスタンスを格納した場合も
子クラスのポインタに、孫クラスのインスタンスを格納した場合も
全てのデストラクタが動くことを確認しました。
オーバーライドした関数についても同様でした。
ですので、私の回答は訂正させていただきます。

以下テストコードです
// 親クラス
class Base{
public:
Base(){
cout << "Base::Base()" << endl;
}
virtual ~Base(){
cout << "Base::~Base()" << endl;
}
virtual void Func(){
cout << "Base::Func()" << endl;
}
};

// 子クラス
class Deriv : public Base{
public:
Deriv(){
cout << "Deriv::Deriv()" << endl;
}
~Deriv(){
cout << "Deriv::~Deriv()" << endl;
}
void Func(){
cout << "Deriv::Func()" << endl;
}
};

// 孫クラス
class Mago : public Deriv{
public:
Mago(){
cout << "Mago::Mago()" << endl;
}
~Mago(){
cout << "Mago::~Mago()" << endl;
}
void Func(){
cout << "Mago::Func()" << endl;
}
};

int main(){
{
Mago mago;
mago.Func();
}
cout << endl;

Base* pBase;
Deriv* pDeriv;
Mago* pMago;

pBase = new Base();
pBase->Func();
delete pBase;

cout << endl;

pBase = new Deriv();
pBase->Func();
delete pBase;

cout << endl;

pBase = new Mago();
pBase->Func();
delete pBase;

cout << endl;

pDeriv = new Deriv();
pDeriv->Func();
delete pDeriv;

cout << endl;

pDeriv = new Mago();
pDeriv->Func();
delete pDeriv;

cout << endl;

pMago = new Mago();
pMago->Func();
delete pMago;

return 0;
}

Re:仮想関数について

Posted: 2010年9月25日(土) 23:07
by 組木紙織
私も訂正をさせていただきます。

>さらに継承をすることを考えているなら必要。そうでないなら必要なし。

さらに継承を考えている場合でも必要はないが、親クラスのvirtualがない場合に注意が必要。


親にvirtualが付いていれば(子ではついていなくても)、孫は親や、子のポインタでポリモーフィズムとして扱うことが出来るが、
親にvirtualが付いていなければ、(子ではvirtualが付いていても)、親クラスのポインタでポリモーフィズムとして扱うことが出来ない。

以下のコードでTYPE1を定義してあれば(TYPE2が定義されてなくても)意図通りの動き方をしますが、
TYPE2だけ定義してあれば、以下のような動作をし、一部意図通りには動きません。
//実行結果
Base
Base
Base
A
B
//ここまで
親でvirtualを消して、子でvirtualをつけ親クラスのポインタで扱う状況は想定できないので、
ほとんど気にしなくてもいいと思います。

#多重継承までは確かめていませんが、いまはそこまで考える必要はないと思うのでここまでとしときます。


/*******************************
TYPE1 Baseにvirtualをつける
TYPE2 Aにvirtualをつける
******************************/
//#define TYPE1
//#define TYPE2

#include <iostream>

class Base
{
public:
#ifdef TYPE1
virtual
#endif
void print(){std::cout << "Base" << std::endl;}
};

class A :public Base
{
public:
#ifdef TYPE2
virtual
#endif
void print(){std::cout << "A" << std::endl;}
};

class B:public A
{
public:
void print(){std::cout << "B" << std::endl;}
};


int main()
{
Base * p_base = new Base;
Base * p_a = new A;
Base * p_b = new B;

p_base->print();
p_a->print();
p_b->print();



A* p_aa =new A;
A* p_ab =new B;
p_aa->print();
p_ab->print();
return 0;

}

Re:仮想関数について

Posted: 2010年9月25日(土) 23:12
by シエル
>組木紙織さん、arrayさん、dicさん、MNSさん、gyzさん。

わかりにくい質問にもかかわらず回答していただき、ありがとうございます^^

ちょっと長くなりそうですので、まとめて返答させていただきます。

>組木紙織さん、array(サウス)さん
分かりやすい返答、サンプルコードありがとうございます。
お二方の回答で分からなかったのが、デフォルトの実装という言葉です。
これはどういう意味でしょうか?
基底クラス単体でインスタンスを作成した場合ということでしょうか?

>dicさん
回答ありがとうございます。非常にわかりやすいサンプル助かりました。

基底クラスのポインタに派生クラスのインスタンスのポインタが格納されている場合、
そのポインタからは、仮想関数化してない派生クラスの関数を呼び出せないことは知りませんでした。
確かにもっと実際に何回か自分でテストしてみたほうが良さそうです。

>MNSさん
回答ありがとうございます。
virtualはつけてもつけなくても特に変わらないんですね。覚えときます。

>gyzさん
回答ありがとうございます。個人的に一番わかりやすかったです^^

わかりやすさを考慮して、派生側にもvirtualをつけることあるんですね。
つけても特に問題ないようですし、それなら納得です。

仮想デストラクタについても、非常にわかりやすいサイトを貼っていただいて、すごい理解できました。
仮想デストラクタ化しないと、問題が出る理由理解できました。
派生側にもvirtualをつけなくても、派生側のデストラクタはちゃんと実行されるんですね。
これを言ってくれなければ、また質問していたとこでした。本当に助かりました^^

皆さんの回答を見て、完全に理解できたのは、質問③、④です。

質問①については、つまり、
仮想関数化するってことは基底クラスのポインタを使って、各クラスの同じ名前の関数を呼び出したい
ってことですよね?
ということは、基底クラスのポインタは使わずに、各クラスそれぞれで、
A a;
B b;
のようにインスタンスを作成すれば、仮想関数化する必要はないってことでよろしいでしょうか?
何でこんなにこの質問にこだわってるかというと、http://marupeke296.com/GDEVSmp_No7_State.htmlに、
SceneBaseっていうクラスを継承して、各シーンのインスタンスをGameScene.hで作成してるんですが、
別に基底クラスのポインタを使ってないのに、なぜ仮想関数化してる関数があるのかが気になったので、
質問させていただいております。

質問②については、
「デフォルトの実装」という言葉の意味が分かれば大丈夫そうです。

返答が遅くなってすみませんでした。本当にありがとうございます。
引き続きよろしくお願い致します

Re:仮想関数について

Posted: 2010年9月25日(土) 23:25
by array
> 質問②については、
>「デフォルトの実装」という言葉の意味が分かれば大丈夫そうです。

デフォルトというのは、多態性を持たせると言っても、数多く継承していけば
動作を書き変えなくても良い派生クラスがでてくるかもしれません。

その時に、基底クラスに定義(デフォルト)あれば、特に書き換える必要が無くなります。

逆に基底クラスに純粋仮想関数(デフォルトがない)があると派生クラスでは必ず実装しないといけなくなります。

---
追記。

ちょっと分りにくかったかもしれないので用語に置き換えると

仮想関数化(デフォルトあり)
純粋仮想関数(デフォルトなし)

と考えても問題ない気がします。(問題ありそうなら有識者の方突っ込んで下さい^^;) 画像

Re:仮想関数について

Posted: 2010年9月25日(土) 23:37
by シエル
>組木紙織さん。gyzさん。

サンプルありがとうございます。すごい理解が深まりました^^

>>サウスさん。
デフォルトの意味理解できました。もう大丈夫です!
ありがとうございました!お疲れ様です!

後は①の質問だけですね。。。
皆さんお忙しいと思うので、暇な時で全然かまいませんので^^;

Re:仮想関数について

Posted: 2010年9月25日(土) 23:44
by gyz
>SceneBaseっていうクラスを継承して、各シーンのインスタンスをGameScene.hで作成してるんですが、
>別に基底クラスのポインタを使ってないのに、なぜ仮想関数化してる関数があるのかが気になったので、
>質問させていただいております。

それはSceneBaseが多態性の実現を目的とした基底クラスでは無いからだと思います
この場合SceneBaseはゲームの各シーンを表すクラスが持つべき、共通かつ最低限の処理を定義しています
具体的にはゲームの状態を更新するupdateメソッドと、
updateメソッド返り値の列挙型です

また、SceneBaseのupdateメソッドは純粋仮想関数になっていますが
コレを継承したクラスは、必ずupdateメソッドを実装しないといけません(たとえ空実装でも)
もし、updateメソッドを実装し忘れると、コンパイル時にエラーとなるので
確実に実装洩れを防げるわけです。
メソッドの数が増えてくれば増えてくるほど、うっかりとしたミスを防ぐ有効な手段になると思います。

あと、継承した各シーン用クラスのupdateがvirtualにしてあるのは
前の質問で回答した通りですね。

Re:仮想関数について

Posted: 2010年9月26日(日) 00:32
by シエル
>>gyzさんありがとうございます!

正に私の求めていた回答です。理解できました^^
ありがとうございます!

これにて全ての質問が解決したので、解決ボタンを押させていただきます。

組木紙織さん、arrayさん、dicさん、MNSさん、gyzさん、ありがとうございました!