web-dev-qa-db-ja.com

コンストラクター内で仮想関数を呼び出す

2つのC++クラスがあるとします。

class A
{
public:
  A() { fn(); }

  virtual void fn() { _n = 1; }
  int getn() { return _n; }

protected:
  int _n;
};

class B : public A
{
public:
  B() : A() {}

  virtual void fn() { _n = 2; }
};

次のコードを書くと:

int main()
{
  B b;
  int n = b.getn();
}

nが2に設定されていることを期待するかもしれません。

nが1に設定されていることがわかります。なぜですか?

218
David Coufal

コンストラクターまたはデストラクターから仮想関数を呼び出すことは危険であり、可能な限り避ける必要があります。すべてのC++実装は、現在のコンストラクターの階層レベルで定義された関数のバージョンを呼び出す必要があります。

C++ FAQ Lite は、セクション23.7でこれを非常に詳細にカバーしています。フォローアップのために、それ(およびFAQの残りの部分)を読むことをお勧めします。

抜粋:

[...]コンストラクターでは、派生クラスからのオーバーライドがまだ行われていないため、仮想呼び出しメカニズムは無効になっています。オブジェクトは、「派生前のベース」から上に構築されます。

[...]

破壊は「基本クラスの前の派生クラス」で行われるため、仮想関数はコンストラクターのように動作します。ローカル定義のみが使用されます。

EDITほとんどすべてに修正(litbに感謝)

201
JaredPar

コンストラクターからポリモーフィック関数を呼び出すことは、ほとんどのOO言語での災害のレシピです。この状況が発生すると、言語によってパフォーマンスが異なります。

基本的な問題は、すべての言語で、派生型の前に基本型を構築する必要があることです。さて、問題はコンストラクターからポリモーフィックメソッドを呼び出すことの意味です。あなたはそれがどのように振る舞うと期待していますか? 2つのアプローチがあります。ベースレベルでメソッドを呼び出す(C++スタイル)、または階層の最下部にある未構築のオブジェクトでポリモーフィックメソッドを呼び出す(Javaの方法)。

C++では、Baseクラスは、独自の構築に入る前に、仮想メソッドテーブルのバージョンを構築します。この時点で、仮想メソッドの呼び出しは、メソッドのベースバージョンを呼び出すか、階層のそのレベルに実装がない場合に純粋な仮想メソッドが呼び出されますを生成します。 Baseが完全に構築されると、コンパイラはDerivedクラスの構築を開始し、メソッドポインターをオーバーライドして、階層の次のレベルの実装を指します。

class Base {
public:
   Base() { f(); }
   virtual void f() { std::cout << "Base" << std::endl; } 
};
class Derived : public Base
{
public:
   Derived() : Base() {}
   virtual void f() { std::cout << "Derived" << std::endl; }
};
int main() {
   Derived d;
}
// outputs: "Base" as the vtable still points to Base::f() when Base::Base() is run

Javaでは、コンパイラーは、BaseコンストラクターまたはDerivedコンストラクターを入力する前に、構築の最初のステップで同等の仮想テーブルを構築します。意味は異なります(そして、私の好みにより危険です)。基本クラスコンストラクターが派生クラスでオーバーライドされるメソッドを呼び出すと、実際には、構築されていないオブジェクトのメソッドを呼び出す派生レベルで呼び出しが処理され、予期しない結果が生じます。コンストラクタブロック内で初期化される派生クラスのすべての属性は、「最終」属性を含めて、まだ初期化されていません。クラスレベルで定義されたデフォルト値を持つ要素には、その値があります。

public class Base {
   public Base() { polymorphic(); }
   public void polymorphic() { 
      System.out.println( "Base" );
   }
}
public class Derived extends Base
{
   final int x;
   public Derived( int value ) {
      x = value;
      polymorphic();
   }
   public void polymorphic() {
      System.out.println( "Derived: " + x ); 
   }
   public static void main( String args[] ) {
      Derived d = new Derived( 5 );
   }
}
// outputs: Derived 0
//          Derived 5
// ... so much for final attributes never changing :P

ご覧のとおり、多態的な(C++の用語で-virtual)メソッドを呼び出すことは、エラーの一般的な原因です。 C++では、少なくとも、まだ構築されていないオブジェクトのメソッドを呼び出さないという保証があります...

その理由は、C++オブジェクトはタマネギのように内部から構築されているからです。スーパークラスは、派生クラスの前に構築されます。したがって、Bを作成する前に、Aを作成する必要があります。 Aのコンストラクターが呼び出されたとき、それはまだBではないため、仮想関数テーブルにはまだAのfn()のコピーのエントリがあります。

54
David Coufal

C++ FAQ Lite これをかなりうまくカバーしています:

基本的に、基本クラスコンストラクターの呼び出し中、オブジェクトはまだ派生型ではないため、派生型ではなく基本型の仮想関数の実装が呼び出されます。

23
Aaron Maenpaa

問題の解決策の1つは、ファクトリメソッドを使用してオブジェクトを作成することです。

  • 仮想メソッドafterConstruction()を含むクラス階層の共通基本クラスを定義します。
 class Object 
 {
 public:
 virtual void afterConstruction(){} 
 // ... 
}; 
  • ファクトリメソッドを定義します。
 template <class C> 
 C * factoryNew()
 {
 C * pObject = new C(); 
 pObject-> afterConstruction( ); 
 
 return pObject; 
} 
  • 次のように使用します。
 class MyClass:public Object 
 {
 public:
 virtual void afterConstruction()
 {
 //何かをします。
} 
 // ... 
}; 
 
 MyClass * pMyObject = factoryNew(); 
 
13
Tobias

C++標準(ISO/IEC 14882-2014) say's:

仮想関数(10.3)を含むメンバー関数は、構築中または破棄中に呼び出すことができます(12.6.2)。クラスの非静的データメンバーの構築中または破棄中を含め、コンストラクターまたはデストラクタから仮想関数が直接または間接的に呼び出され、呼び出しが適用されるオブジェクトが構築中のオブジェクト(xと呼ぶ)である場合または破棄、呼び出された関数は、コンストラクターまたはデストラクターのクラスの最後のオーバーライドであり、より派生したクラスでオーバーライドするものではありません。仮想関数呼び出しが明示的なクラスメンバーアクセス(5.2.5)を使用し、オブジェクト式がxまたはそのオブジェクトの基本クラスサブオブジェクトの完全なオブジェクトを参照し、xまたはその基本クラスサブオブジェクトのいずれかを参照しない場合、動作はundefined

そのため、構築中または破棄中のオブジェクトを呼び出そうとするコンストラクターまたはデストラクターからvirtual関数を呼び出さないでください。構築の順序はbaseから派生およびデストラクターの順序は、から派生して基底クラスに派生します。

したがって、構築中の基本クラスから派生クラス関数を呼び出そうとすると危険です。同様に、オブジェクトは構築とは逆の順序で破棄されるため、デストラクタからより派生クラスの関数を呼び出そうとすると、すでに存在するリソースにアクセスする可能性がありますリリースされました。

1
msc

virtual関数呼び出しがコンストラクターから呼び出されたときに期待どおりに動作しない理由については、既に他の回答で説明されています。代わりに、基本型のコンストラクターからポリモーフィックのような動作を得るための別の可能な回避策を提案したいと思います。

テンプレートコンストラクターを基本型に追加して、テンプレート引数が常に派生型であると推測されるようにすることで、派生型の具象型を認識することができます。そこから、その派生型のstaticメンバー関数を呼び出すことができます。

このソリューションでは、static以外のメンバー関数を呼び出すことはできません。実行は基本型のコンストラクターで行われますが、派生型のコンストラクターは、そのメンバーの初期化リストを調べる時間すらありませんでした。作成されるインスタンスの派生型部分は、初期化され始めていません。また、nonstaticメンバー関数はほぼ確実にデータメンバーとやり取りするため、派生型のnonstaticメンバー関数を基本型のコンストラクターから呼び出すことはwantには珍しいことです。

サンプル実装は次のとおりです。

#include <iostream>
#include <string>

struct Base {
protected:
    template<class T>
    explicit Base(const T*) : class_name(T::Name())
    {
        std::cout << class_name << " created\n";
    }

public:
    Base() : class_name(Name())
    {
        std::cout << class_name << " created\n";
    }


    virtual ~Base() {
        std::cout << class_name << " destroyed\n";
    }

    static std::string Name() {
        return "Base";
    }

private:
    std::string class_name;
};


struct Derived : public Base
{   
    Derived() : Base(this) {} // `this` is used to allow Base::Base<T> to deduce T

    static std::string Name() {
        return "Derived";
    }
};

int main(int argc, const char *argv[]) {

    Derived{};  // Create and destroy a Derived
    Base{};     // Create and destroy a Base

    return 0;
}

この例は印刷する必要があります

Derived created
Derived destroyed
Base created
Base destroyed

Derivedが構築されるとき、Baseコンストラクターの動作は、構築されるオブジェクトの実際の動的型に依存します。

1

Windowsエクスプローラーからのクラッシュエラーを知っていますか?! "純粋な仮想関数呼び出し..."
同じ問題...

class AbstractClass 
{
public:
    AbstractClass( ){
        //if you call pureVitualFunction I will crash...
    }
    virtual void pureVitualFunction() = 0;
};

関数pureVitualFunction()の実装がなく、関数がコンストラクターで呼び出されるため、プログラムがクラッシュします。

1
TimW

Vtableはコンパイラーによって作成されます。クラスオブジェクトには、vtableへのポインタがあります。開始すると、そのvtableポインターは基本クラスのvtableを指します。コンストラクターコードの最後に、コンパイラーはクラスの実際のvtableへのvtableポインターを再ポイントするコードを生成します。これにより、仮想関数を呼び出すコンストラクターコードが、クラスのオーバーライドではなく、それらの関数の基本クラス実装を呼び出すことが保証されます。

1
Yogesh

指摘されているように、オブジェクトは構築時に作成されます。ベースオブジェクトが構築されているとき、派生オブジェクトはまだ存在しないため、仮想関数のオーバーライドは機能しません。

ただし、これは、ゲッターが定数を返す場合、または静的メンバーで表現できる場合、仮想関数の代わりにstatic polymorphismを使用するポリモーフィックゲッターで解決できます関数、この例ではCRTPを使用します( https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern )。

template<typename DerivedClass>
class Base
{
public:
    inline Base() :
    foo(DerivedClass::getFoo())
    {}

    inline int fooSq() {
        return foo * foo;
    }

    const int foo;
};

class A : public Base<A>
{
public:
    inline static int getFoo() { return 1; }
};

class B : public Base<B>
{
public:
    inline static int getFoo() { return 2; }
};

class C : public Base<C>
{
public:
    inline static int getFoo() { return 3; }
};

int main()
{
    A a;
    B b;
    C c;

    std::cout << a.fooSq() << ", " << b.fooSq() << ", " << c.fooSq() << std::endl;

    return 0;
}

静的ポリモーフィズムを使用すると、基本クラスは、コンパイル時に情報が提供されるため、どのクラスのゲッターを呼び出すかを認識します。

0
stands2reason

最初に、オブジェクトが作成され、次にそのアドレスをポインターに割り当てます。コンストラクターはオブジェクトの作成時に呼び出され、データメンバーの値を初期化するために使用されます。オブジェクト作成後、オブジェクトへのポインターがシナリオに入ります。そのため、C++ではコンストラクタをvirtualにすることはできません。もう1つの理由は、仮想関数のプロパティの1つがポインターによってのみ使用できるため、仮想コンストラクターを指すコンストラクターへのポインターのようなものがないことです。

  1. コンストラクターは静的であるため、仮想関数は値を動的に割り当てるために使用されるため、仮想化することはできません。
0
Priya