6

私はEffective C++を読んでいますが、「アイテム9:構築または破棄中に仮想関数を呼び出さないでください」があります。そして、このルールに違反したとしても、私のコードは問題ないかどうか疑問に思っています:

using namespace std;

class A{
    public:
        A(bool doLog){
            if(doLog)
               log();
        }

        virtual void log(){
            cout << "logging A\n";
        }
};


class B: public A{
public:
    B(bool doLog) : A(false){
        if(doLog)
            log();
    }

    virtual void log(){
        cout << "logging B\n";
    }
};


int main() {
    A a(true);
    B b(true);
}

このアプローチに何か問題がありますか?もっと複雑なことをすると、トラブルになることはありますか?

ほとんどの回答は私がそこで行ったことが得られなかったようで、コンストラクターから仮想関数を呼び出すことが危険な可能性がある理由を簡単に説明しました。

私のプログラムの出力が次のようになることを強調したいと思います。

logging A
logging B

したがって、構築時に A がログに記録され、構築時に B がログに記録されます。そして、それが私が欲しいものです!しかし、コンストラクターで仮想関数を呼び出す際の問題を克服するために、私の「ハック」で何か問題がある (潜在的に危険な) ものを見つけたかどうかを尋ねています。

4

3 に答える 3

15

このアプローチに何か問題がありますか?

Bjarne Stroustrup からの回答:

コンストラクターから仮想関数を呼び出すことはできますか?

はい、しかし気をつけてください。期待どおりに動作しない場合があります。コンストラクターでは、派生クラスからのオーバーライドがまだ行われていないため、仮想呼び出しメカニズムは無効になっています。オブジェクトは、「派生前のベース」のベースから構築されます。検討:

    #include<string>
    #include<iostream>
    using namespace std;

class B {
public:
    B(const string& ss) { cout << "B constructor\n"; f(ss); }
    virtual void f(const string&) { cout << "B::f\n";}
};

class D : public B {
public:
    D(const string & ss) :B(ss) { cout << "D constructor\n";}
    void f(const string& ss) { cout << "D::f\n"; s = ss; }
private:
    string s;
};

int main()
{
    D d("Hello");
}

プログラムはコンパイルして生成します

B constructor
B::f
D constructor

D::f ではないことに注意してください。D::f() が B::B() から呼び出されるようにルールが異なるとどうなるかを考えてみましょう: コンストラクター D::D() がまだ実行されていないため、D::f() はその引数を初期化されていない文字列 s に割り当てようとします。その結果、すぐにクラッシュする可能性が高くなります。破棄は「基本クラスの前に派生クラス」で行われるため、仮想関数はコンストラクターのように動作します。ローカル定義のみが使用され、オーバーライド関数の呼び出しは行われず、オブジェクトの (現在破棄されている) 派生クラス部分に触れないようにします。

詳細については、D&E 13.2.4.2 または TC++PL3 15.4.3 を参照してください。

このルールは実装アーティファクトであることが示唆されています。それはそんなに。実際、他の関数とまったく同じように、コンストラクターから仮想関数を呼び出すという安全でないルールを実装する方がはるかに簡単です。ただし、これは、基本クラスによって確立された不変条件に依存する仮想関数を作成できないことを意味します。それはひどい混乱になるでしょう。

于 2013-05-23T16:47:33.957 に答える
11

そして、このルールに違反していても、私のコードが問題ないかどうか疑問に思っています:

それはあなたが「大丈夫」の意味に依存します。あなたのプログラムは整形式であり、その動作は明確に定義されているため、未定義の動作などを呼び出すことはありません。

ただし、仮想関数の呼び出しを見ると、その関数をオーバーライドする最も派生した型によって提供される実装を呼び出すことによって呼び出しが解決されると予想される場合があります。

ただし、構築中は対応するサブオブジェクトがまだ構築されていないため、最も派生したサブオブジェクトが現在構築中のサブオブジェクトになります。結果: 関数が仮想関数ではないかのように呼び出しがディスパッチされます。

これは直感に反するため、プログラムはこの動作に依存すべきではありません。したがって、読み書きのできるプログラマーは、このようなパターンを回避し、Scott Meyer のガイドラインに従うことに慣れる必要があります。

于 2013-05-23T16:48:28.897 に答える
4

明確に定義されているという意味で「結構」です。期待どおりに動作するという意味では「大丈夫」ではないかもしれません。

最終的なオーバーライドではなく、現在構築中 (または破棄中) のクラスからオーバーライドを呼び出します。最終的な派生クラスはまだ構築されていない (または既に破棄されている) ため、アクセスできません。したがって、最終的なオーバーライドをここで呼び出すと、問題が発生する可能性があります。

この動作は混乱を招く可能性があるため、実行する必要はありません。そのような状況でサブクラス化するのではなく、集約によってクラスに動作を追加することをお勧めします。クラス メンバーはコンストラクタ本体の前に構築され、デストラクタの後まで続くため、これらの両方の場所で使用できます。

してはいけないことの 1 つは、仮想関数がそのクラスで純粋な仮想関数である場合に、コンストラクタまたはデストラクタから仮想関数を呼び出すことです。それは未定義の動作です。

于 2013-05-23T17:00:56.803 に答える