合計 昨日 今日

(C++)インターフェースに純粋仮想デストラクタが無いと継承時にデストラクタが呼ばれない?

[このトピックは解決済みです]

フォーラムルール
フォーラムルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
Name: IIICE
[URL]
Date: 2017年9月12日(火) 13:49
No: 1
(OFFLINE)

 (C++)インターフェースに純粋仮想デストラクタが無いと継承時にデストラクタが呼ばれない?

C++のインターフェースクラスを使う練習中です。
開発環境はVisual Studio Community 2015です。

1つのインターフェースクラス「IHoge」から派生される
2つのクラス「CHoge1」「CHoge2」があります。
各クラスの共通部分(インターフェース)を取得するためにGetIHoge関数を用意し、
ここではメモリをnewにより確保し、そのポインタを返します。

動作確認のために、「CHoge1」「CHoge2」それぞれのデストラクタにcoutを用意しました。

以下がそのコードです。
コード[C++]: 全て選択
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <memory>
 
/* Hogeインターフェースクラス */
class IHoge {
public:
    //純粋仮想デストラクタ
    virtual ~IHoge() = 0;
};
IHoge::~IHoge() {}
 
/* IHoge継承クラス1 */
class CHoge1 : public IHoge {
public:
    virtual ~CHoge1() override {
        std::cout << "デストラクタ:CHoge1" << std::endl;
    }
};
 
/* IHoge継承クラス2 */
class CHoge2 : public IHoge {
public:
    virtual ~CHoge2() override {
        std::cout << "デストラクタ:CHoge2" << std::endl;
    }
};
 
/* CHoge1,CHoge2のインターフェース取得用関数 */
IHoge* GetIHoge(int num) {
    switch (num)
    {
    case 1:
        return new CHoge1;
    case 2:
        return new CHoge2;
    default:
        return nullptr;
    }
}
 
/* メイン */
int main() {
    IHoge* pIHoge;
 
    //CHoge2としてメモリ確保(引数に「2」を渡す)
    if ((pIHoge = GetIHoge(2)) == nullptr)
        return 1; //エラー
 
    //解放
    delete pIHoge;
 
    return 0;
}


実行結果は、deleteされた時点で「デストラクタ:CHoge2」と出力され、
正常にCHoge2のデストラクタが呼ばれていることが確認できます。

そこで、インターフェースクラスの純粋仮想デストラクタが無かった場合、すなわち

コード[C++]: 全て選択
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Hogeインターフェースクラス */
class IHoge {
public:
    //純粋仮想デストラクタ
    //virtual ~IHoge() = 0; //←無かった場合
};
//IHoge::~IHoge() {} //実体もコメントアウト
 
/* IHoge継承クラス1 */
class CHoge1 : public IHoge {
public:
    //virtual ~CHoge1() override {
    ~CHoge1() { //←通常のデストラクタに変更
        std::cout << "デストラクタ:CHoge1" << std::endl;
    }
};
 
/* IHoge継承クラス2 */
class CHoge2 : public IHoge {
public:
    //virtual ~CHoge2() override {
    ~CHoge2() { //←通常のデストラクタに変更
        std::cout << "デストラクタ:CHoge2" << std::endl;
    }
};


各クラスに上記の変更を加えて実行すると、
deleteされた時点でCHoge2のデストラクタから何も出力されなくなりました。
つまりCHoge2のデストラクタが呼ばれていないように思います。

インターフェースクラスには無いものの継承クラス「CHoge1」「CHoge2」の中ではデストラクタを定義しているにもかかわらず、
なぜデストラクタが呼ばれなくなるのでしょうか。

CHoge2からIHogeインターフェースクラスにキャスト的なことが成されたために、
IHogeでデストラクタが定義されていない
→CHoge2のデストラクタが見えない(実行されない)
ということでしょうか。

Name: Math
[URL]
Date: 2017年9月12日(火) 18:54
No: 2
(OFFLINE)

 Re: (C++)インターフェースに純粋仮想デストラクタが無いと継承時にデストラクタが呼ばれない?

純粋仮想関数にできるメンバ関数が1つも無いとき デストラクタを純粋仮想にします。 このようなデストラクタは、純粋仮想デストラクタと呼ばれます。
ただし、純粋仮想デストラクタは、「中身は空でも良いので、定義を書かなければなりません。」
コード[C++]: 全て選択
1
2
3
4
5
6
7
8
class Base {
public:
    virtual ~Base() = 0;
};
 
Base::~Base()
{
}

Windows10、VS2017Communityではコメントアウトした時点でエラー表示がでますね。
画像

Name: Math
[URL]
Date: 2017年9月12日(火) 19:05
No: 3
(OFFLINE)

 Re: (C++)インターフェースに純粋仮想デストラクタが無いと継承時にデストラクタが呼ばれない?

少し違ってますね!。
抽象クラスにしたいが、純粋仮想関数にできるメンバ関数が1つも無いという状況があり得ます。その場合は、無理に仮想関数をひねり出すよりも、デストラクタを純粋仮想にするのが得策です。 このようなデストラクタは、純粋仮想デストラクタと呼ばれます。
コード[C++]: 全て選択
1
2
3
4
5
6
7
/* Hogeインターフェースクラス */
class IHoge {
public:
    //純粋仮想デストラクタ
    //virtual ~IHoge() = 0; //←無かった場合
};
//IHoge::~IHoge() {} //実体もコメントアウト

はメンバ関数が1つも無い。

Name: IIICE
[URL]
Date: 2017年9月12日(火) 20:25
No: 4
(OFFLINE)

 Re: (C++)インターフェースに純粋仮想デストラクタが無いと継承時にデストラクタが呼ばれない?

ご回答ありがとうございます。

先に、
Windows10、VS2017Communityではコメントアウトした時点でエラー表示がでますね。

これについては、仰る通りインターフェースクラスの純粋仮想デストラクタを
コメントアウトした時点でエラーが出ます。
そのため、そのコメントアウトに合わせて
コード[C++]: 全て選択
1
2
3
4
5
6
7
8
/* IHoge継承クラス1 */
class CHoge1 : public IHoge {
public:
    //virtual ~CHoge1() override {
    ~CHoge1() { //←通常のデストラクタに変更
        std::cout << "デストラクタ:CHoge1" << std::endl;
    }
};

のように、オーバーライドではなく通常のデストラクタの宣言に変更する必要があります。
それは元の質問文にもコメントで記述してあります。


まぁ後述の「純粋仮想関数が1つも無いインターフェースクラス」の存在自体は防ぐべきであるとは思います。つまり、せめてインターフェースクラスには純粋仮想デストラクタを最低限入れ、インターフェースクラスであることを明示すべきですよね。

それは良いのですが、
それでも各継承クラスに用意したデストラクタに処理が入らないのは少し不思議に思います。
コードを縮小すれば、以下のコードを実行していることになります。
コード[C++]: 全て選択
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <memory>
using namespace std;
 
/* Hogeインターフェースクラス */
class IHoge {
};
 
/* IHoge継承クラス1 */
class CHoge1 : public IHoge {
public:
    ~CHoge1() {
        cout << "CHoge1" << endl;
    }
};
 
/* CHoge1のインターフェース取得用関数 */
IHoge* GetIHoge() {
    return new CHoge1;
}
 
/* メイン */
int main() {
    IHoge* pIHoge = GetIHoge();
    delete pIHoge; //ここでCHoge1のデストラクタが呼ばれないのはなぜ?
    return 0;
}

ここまで来るとインターフェースクラスはいらなくなってしまうのですが、
インターフェースクラスに仮想デストラクタを用意しない通常の継承では、
なぜ継承クラスのデストラクタが呼ばれなくなってしまうのでしょうか。

---
(ここの掲示板はソースコードが見やすくて本当にいいですね…感動)

Name: Math
[URL]
Date: 2017年9月12日(火) 21:56
No: 5
(OFFLINE)

 Re: (C++)インターフェースに純粋仮想デストラクタが無いと継承時にデストラクタが呼ばれない?

•virtual なメソッドを持つクラスは、その子クラスのポインタを親クラスのポインタとして必ず利用するが、
•子クラスを親クラスのポインタとして利用する場合、デストラクタは virtual でなくてはならない

つまりデストラクタが virtual でない場合、子クラスのポインタを親クラスのポインタにキャストして使用してはいけません。 なぜならデストラクタが virtual でない場合、親クラスの型のポインタを delete した際には親クラスのデストラクタしか呼ばれないからです。 たとえ親クラスの型のポインタが指している実体が子クラスだったとしても子クラスのデストラクタ (これは暗黙的に親クラスのデストラクタを呼ぶ) は呼び出されません。

// ~Parent is not virtual.
Parent* parent = new Child();
...
delete parent; // this always calls ~Parent(); ~Child() is never called.

このコードでは Child のデストラクタが呼び出されないので Child のリソースの解放が行われません。 もちろん delete parent; を呼ばなければ問題は起こらないので、 delete parent; しないように気をつけていれば Child のポインタを Parent* に代入すること自体は問題ないが、 間違えやすい上に間違えた場合にはメモリーリーク系の厄介なバグの原因になるので 親クラスのポインタにキャストすること自体を避けるべきです。

なお、暗黙的に作られるデストラクタは virtual ではないのでデストラクタが空の場合でも virtual なデストラクタを明示的に定義しなくてならない点に気をつけて下さい:

という事のようです。

Name: IIICE
[URL]
Date: 2017年9月13日(水) 00:22
No: 6
(OFFLINE)

 Re: (C++)インターフェースに純粋仮想デストラクタが無いと継承時にデストラクタが呼ばれない?

[解決!]

調べてみてもやはりそういうことのようですね。
インターフェースクラスを使うとオーバーライドすべきメソッドを指定できるので機能拡張には大変便利に思いますが、
本問題には気をつけなくてはなりませんね。

ご回答いただきありがとうございました。


Return to C言語何でも質問掲示板

オンラインデータ

このフォーラムを閲覧中のユーザー: なし & ゲスト[13人]