C++の仮想メンバ関数。主に仮想デストラクタ。

先日、継承の階層が3層(親・子・孫)となっている場合、子のクラスのデストラクタにvirtualは必要なのか気になったので実験してみました。
ついでにデストラクタ以外の普通のメンバ関数についても検証。
まず以下のようなクラスとmain関数を用意。

#include <iostream>

using namespace std;

class sample_base{
    public:
    sample_base(){
        cout << "construct sample_base." << endl;
    }
    virtual ~sample_base(){
        cout << "destruct sample_base." << endl;
    }

    virtual void echo(){
        cout << "わんわん" << endl;
    }
};

class sample_drived1 : public sample_base{
    public:
    sample_drived1(){
        cout << "\tconstruct sample_drived1." << endl;
    }
    ~sample_drived1(){
        cout << "\tdestruct sample_drived1." << endl;
    }

    void echo(){
        cout << "にゃんにゃん" << endl;
    }
};

class sample_drived2 : public sample_drived1{
    public:
    sample_drived2(){
        cout << "\t\tconstruct sample_drived2." << endl;
    }
    ~sample_drived2(){
        cout << "\t\tdestruct sample_drived2." << endl;
    }

    void echo(){
        cout << "もーもー" << endl;
    }
};

int main(){
    
    cout << "-- delete sample_base pointer. --" << endl;
    sample_base *p_base = new sample_drived2();
    p_base->echo();
    cout << "size: " << sizeof(*p_base) << endl;
    delete p_base;

    cout << endl << "-- delete sample_drived1 pointer. --" << endl;
    sample_drived1 *p_d1 = new sample_drived2();
    p_d1->echo();
    cout << "size: " << sizeof(*p_d1) << endl;
    delete p_d1;

    return 0;
}

出力結果は以下のようになる。

-- delete sample_base pointer. --
construct sample_base.
        construct sample_drived1.
                construct sample_drived2.
もーもー
size: 8
                destruct sample_drived2.
        destruct sample_drived1.
destruct sample_base.

-- delete sample_drived1 pointer. --
construct sample_base.
        construct sample_drived1.
                construct sample_drived2.
もーもー
size: 8
                destruct sample_drived2.
        destruct sample_drived1.
destruct sample_base.

sample_baseのポインタでdeleteした場合は当然として、sample_drived1のポインタでdeleteした場合も正常にデストラクトされている。
オブジェクトサイズから見ても仮想関数テーブルがきちんと認識されている事が分かる。
また、デストラクタ同様にechoメンバ関数についてもオブジェクト本来の型の物が呼ばれている。


次に上で用意したクラスのvirtualの位置を変更する。
sample_baseのメンバに付いているvirtualをsample_drived1のメンバへ移動する。
以下のようなコードになる。

#include <iostream>

using namespace std;

class sample_base{
    public:
    sample_base(){
        cout << "construct sample_base." << endl;
    }
    ~sample_base(){
        cout << "destruct sample_base." << endl;
    }

    void echo(){
        cout << "わんわん" << endl;
    }
};

class sample_drived1 : public sample_base{
    public:
    sample_drived1(){
        cout << "\tconstruct sample_drived1." << endl;
    }
    virtual ~sample_drived1(){
        cout << "\tdestruct sample_drived1." << endl;
    }

    virtual void echo(){
        cout << "にゃんにゃん" << endl;
    }
};

class sample_drived2 : public sample_drived1{
    public:
    sample_drived2(){
        cout << "\t\tconstruct sample_drived2." << endl;
    }
    ~sample_drived2(){
        cout << "\t\tdestruct sample_drived2." << endl;
    }

    void echo(){
        cout << "もーもー" << endl;
    }
};

int main(){
    
    cout << "-- delete sample_base pointer. --" << endl;
    sample_base *p_base = new sample_drived2();
    p_base->echo();
    cout << "size: " << sizeof(*p_base) << endl;
    delete p_base;

    cout << endl << "-- delete sample_drived1 pointer. --" << endl;
    sample_drived1 *p_d1 = new sample_drived2();
    p_d1->echo();
    cout << "size: " << sizeof(*p_d1) << endl;
    delete p_d1;

    return 0;
}

このコードの実行結果は以下のようになる。

-- delete sample_base pointer. --
construct sample_base.
        construct sample_drived1.
                construct sample_drived2.
わんわん
size: 1
destruct sample_base.

-- delete sample_drived1 pointer. --
construct sample_base.
        construct sample_drived1.
                construct sample_drived2.
もーもー
size: 8
                destruct sample_drived2.
        destruct sample_drived1.
destruct sample_base.

仮想デストラクタを持たないsample_baseのポインタでdeleteを行った場合に正常にデストラクトされていない。
C++的には正常だけど、期待される動作とは違うことは明らか。
対してsample_drived1のポインタでdeleteを行った場合には正常にオブジェクト本来の型のデストラクタから順に呼ばれている。
また、sample_baseのポインタが指すオブジェクトのサイズは1だと判断されており、仮想関数テーブルが完全にシカトされている。


次に、デストラクタはsample_baseからvirtualにし、echo()メンバ関数はsample_derived1からvirtualにしてみる。

#include <iostream>

using namespace std;

class sample_base{
    public:
    sample_base(){
        cout << "construct sample_base." << endl;
    }
    virtual ~sample_base(){
        cout << "destruct sample_base." << endl;
    }

    void echo(){
        cout << "わんわん" << endl;
    }
};

class sample_drived1 : public sample_base{
    public:
    sample_drived1(){
        cout << "\tconstruct sample_drived1." << endl;
    }
    ~sample_drived1(){
        cout << "\tdestruct sample_drived1." << endl;
    }

    virtual void echo(){
        cout << "にゃんにゃん" << endl;
    }
};

class sample_drived2 : public sample_drived1{
    public:
    sample_drived2(){
        cout << "\t\tconstruct sample_drived2." << endl;
    }
    ~sample_drived2(){
        cout << "\t\tdestruct sample_drived2." << endl;
    }

    void echo(){
        cout << "もーもー" << endl;
    }
};

int main(){
    
    cout << "-- delete sample_base pointer. --" << endl;
    sample_base *p_base = new sample_drived2();
    p_base->echo();
    cout << "size: " << sizeof(*p_base) << endl;
    delete p_base;

    cout << endl << "-- delete sample_drived1 pointer. --" << endl;
    sample_drived1 *p_d1 = new sample_drived2();
    p_d1->echo();
    cout << "size: " << sizeof(*p_d1) << endl;
    delete p_d1;

    return 0;
}

出力結果は以下のようになる。

-- delete sample_base pointer. --
construct sample_base.
        construct sample_drived1.
                construct sample_drived2.
わんわん
size: 8
                destruct sample_drived2.
        destruct sample_drived1.
destruct sample_base.

-- delete sample_drived1 pointer. --
construct sample_base.
        construct sample_drived1.
                construct sample_drived2.
もーもー
size: 8
                destruct sample_drived2.
        destruct sample_drived1.
destruct sample_base.

この結果から分かることは、仮想メンバ関数の呼び出し時にオブジェクト本来の型のメンバ関数が呼ばれるためには、継承ツリー上で仮想メンバ関数が宣言されているクラス以下のクラス型のポインタから呼び出す必要があることが分かる。
試してないけど参照でも同じ振る舞いをすると思う。むしろ違ったら困る。


結論:
ポリモルフィズム的に扱う事前提に継承されるクラス(A)のメンバ関数(デストラクタ含む)にvirtualを付ければ、継承ツリーで(A)以下のクラスのポインタ・参照から仮想メンバ関数を呼び出す場合、派生クラスにvirtualを付けずともオブジェクト本来の型のメンバ関数が呼び出される。
上記状況で、継承ツリーで(A)より上位のクラスのポインタ・参照から(A)で定義されている仮想メンバ関数を呼び出す場合、同名のメンバ関数が定義されていればそれが、定義されていなければコンパイルエラーかリンクエラーが発生する。


ここまで書いてすげぇ当たり前の事書いてる気がしてきた上に、実はこれ違いますとか言われたらすごい恥ずかしい。

QtScriptがすごく面白そう

だいぶQtにも慣れてきたのでQtScriptをいじり始めてみました。

QScriptEngine engine;
QString script("1+2"), result;
result = engine.evaluate( script ).toString();

これでresultに文字列の3がセットされます。
QtScriptはECMA Scriptをベースにしているので、構文はActionScriptとかJavaScriptとかと同じ。
"1+2"の部分を関数定義しまくって制御構文使いまくりの複雑な物にしても動きます。
ただしQtScriptからQtのクラスや自分で定義したクラスなんかを呼べるようにするにはQScriptEngine::evaluate()を呼ぶ前に色々やらないといけないそうです。
でもネット上の解説サイトでざっと読んだ感じでは大して難しくなさそう。
assistantでしっかり読んでからテストしてみる予定。

new A;した時にAのコンストラクタが例外を投げたらどうなるのか

C++でコンストラクタから例外を送出したい時 - 足跡の解決編です。
手元にあったC++Effective第三版の最後の方のページに全ての答えが書いてありました。
買った本はちゃんと全部読まないとダメですね。


コンストラクタから例外を送出する可能性のあるクラスAがあるとします。
このクラスを

A *obj = new A;

したとき、以下のような事が言えます。
まずnewはAのインスタンス用のメモリを確保してからAのインスタンスを確保したメモリ上に作成します。
なんらかの理由によってAのコンストラクタから例外が送出された場合、C++ランタイムはAのインスタンス用に確保されたメモリを破棄する為に、実行したnewに対応するdeleteを呼び出します。ただしAはインスタンス化していないのでデストラクタは呼ばれません。
これは特にクラス側でnewをオーバーライドしていなければ、C++標準のnewとdeleteのセットということになります。
このC++ランタイムから自動で呼ばれたdeleteによって、Aのインスタンス用に確保されたメモリ領域は破棄されます。
それに伴ってその時点でインスタンス化しているAの内部オブジェクト(メンバ)は全て破棄され各々のメンバのデストラクタが呼ばれます。
したがって、もしコンストラクタ内で動的に領域を確保していた場合、コンストラクタから例外が送出されると、ポインタ変数は破棄されますが動的に確保した領域はdeleteされず宙ぶらりんな状態になります。
また、A自体はデストラクタが呼ばれませんので、デストラクタにdelete用のコードを用意していても華麗にスルーされてしまうので注意が必要です。
なので、コンストラクタ内で動的に確保したメモリに関しては、コンストラクタ内で例外が発生した場合にコンストラクタ自身が責任を持って処分する必要があります。


なお、newがオーバーライドされていた場合、同一の引数を持つdeleteが存在しないとAのインスタンス用にnewが確保した領域は破棄される事なくメモリ上に存在し続けてしまいメモリリークしてしまいます。

C++でコンストラクタから例外を送出したい時

きっと今年最後の思いつき。
一般にコンストラクタから例外を投げるとリソース漏れの危険性が出てくる。
例外を投げた当のオブジェクトが自身のリソースをきちんと処理していなかったり、コンストラクタから例外を投げるクラスをnewした時にインスタンスが宙ぶらりんになっちゃったり。
なのでリソース漏れが嫌ならコンストラクタから絶対に例外を投げないようにすべき。
だがそうすると今度はpublicなエラーチェックメソッドを設ける必要が出たり、空のオブジェクトが存在する事になってしまったりする。
そうすると色々手間が増えてしまったりスマートさが無くなって残念な気持ちになってしまう。表


--- 追記ここから ---
コンストラクタから例外を投げる時、そのクラスがnewされたならインスタンス用に割り当てようとした領域は自動で破棄されるらしい。デストラクタ呼ばれないけど。
破棄されるっていう記述も破棄されないっていう記述も日本語資料じゃ同程度にしかネットで見つからない。英語資料の山から探し出すなんて嫌になっちゃうのでやりません。日本語でさえググった結果何十ページも見るのはしんどかったのに。
問題なのはそれが仕様なのかどうかという事。規格書読めって事なんだろうけど規格書ドコー。
とりあえずはコンストラクタ内で例外を処理しきってしまう方向で頑張る。
--- 追記ここまで ---

--- 追記(09/12/31)ここから ---
一番気になっていた事柄の回答がEffectiveC++第三版の52項に記載されていました。
コンストラクタで例外を投げるクラスAが存在する時、new A;でAのコンストラクタが例外を投げると、A用に確保されたメモリ領域は実行したnewに対応するdeleteが自動で呼び出されてA用のメモリ領域がdeleteされます。
ただし、Aはコンストラクトが完了する前に例外を送出していますので、deleteされてもデストラクタは呼ばれません。基底クラスのデストラクタは呼ばれるかもしれません。
またそのdelete作業中にAのコンストラクタ内でインスタンス化に成功しているメンバは次々と破棄されていき、デストラクタが呼ばれていきます。
--- 追記(09/12/31)ここまで ---


なのでデザパタのファクトリパターン使えばいいんじゃなかろうか。

class SampleClass
{
    private:
        SampleDataClass *m_data;
        bool m_valid;
    public:
        class Exception {};
        static SampleClass* create(const SampleDataClass &data)
        {
            try
            {
                SampleClass *obj = new SampleClass(data);
            }
            catch(std::bac_alloc)
            {
                throw Exception;
            }
      if(!obj->m_valid)
            {
                throw Exception;
            }
            return obj;
        }
        SampleClass(const SampleDataClass &data)
            : m_data(NULL)
            , m_valid(true)
        {
            try
            {
                m_data = new SampleDataClass(data);
            }
            catch(std::bad_alloc)
            {
                m_valid = false;
            }
        }
        ~SampleClass()
        {
            delete m_data;
        }
};

newするメンバが複数必要ならスマートポインタを使えば良し。
んでこれを使う側は下の通り。

try
{
    std::auto_ptr<SampleClass> pointer(SampleClass::create(data));
}
catch(SampleClass::Exception)
{
    // 何かする必要があればする。
}

これでリソース漏れとも完全にオサラバじゃね?
欠点はたくさんインスタンス作りたい時に処理が遅くなること。


何か得意気に書いてから思ったけど、これくらい普通に行われてますよねー。
しかも自分で書いておいて何ですが、このやり方はどこかで見たような気がしてしょうがない。


本題とは全く関係が無いけど、Qtのクラスって例外投げるのだろうか・・・。

属性?みたいなの

メソッドでの場合はすぐ忘れるからメモ。
virtual - クラス定義の中でだけvirtualを書く。
static - 同上。
inline - クラス定義内で定義したメソッドには不要。そうじゃない場合は同一ファイル内で関数定義してる場合のみ有効。