web-dev-qa-db-ja.com

基本クラスコンストラクターからの純粋仮想関数の呼び出し

純粋な仮想関数を含む基本クラスMyBaseがあります。

void PrintStartMessage() = 0

派生クラスごとにコンストラクタでそれを呼び出すようにしたい

それから私はそれを基本クラス(MyBase)コンストラクタに入れます

 class MyBase
 {
 public:

      virtual void PrintStartMessage() =0;
      MyBase()
      {
           PrintStartMessage();
      }

 };

 class Derived:public MyBase
 {     

 public:
      void  PrintStartMessage(){

      }
 };

void main()
 {
      Derived derived;
 }

しかし、リンカエラーが発生します。

 this is error message : 

 1>------ Build started: Project: s1, Configuration: Debug Win32 ------
 1>Compiling...
 1>s1.cpp
 1>Linking...
 1>s1.obj : error LNK2019: unresolved external symbol "public: virtual void __thiscall MyBase::PrintStartMessage(void)" (?PrintStartMessage@MyBase@@UAEXXZ) referenced in function "public: __thiscall MyBase::MyBase(void)" (??0MyBase@@QAE@XZ)
 1>C:\Users\Shmuelian\Documents\Visual Studio 2008\Projects\s1\Debug\s1.exe : fatal error LNK1120: 1 unresolved externals
 1>s1 - 2 error(s), 0 warning(s)

すべての派生クラスに強制したい...

A- implement it

B- call it in their constructor 

どうすればよいですか?

39

C++のコンストラクターとデストラクターで仮想関数を呼び出すべきではない理由を説明する記事はたくさんあります。そのような呼び出し中に舞台裏で何が起こるかについての詳細は、 ここここ を見てください。

つまり、オブジェクトはベースから派生まで構築されます。したがって、基本クラスのコンストラクターから仮想関数を呼び出そうとしても、派生したコンストラクターがまだ呼び出されていないため、派生クラスからのオーバーライドはまだ行われていません。

34
a1ex07

そのオブジェクトがまだ構築されている間に派生から純粋な抽象メソッドを呼び出そうとすることは安全ではありません。それは車にガスを入れようとするようなものですが、その車はまだ組立ラインにあり、ガスタンクはまだ投入されていません。

そのようなことを行うのに最も近い方法は、最初にオブジェクトを完全に構築し、その後でメソッドを呼び出すことです。

template <typename T>
T construct_and_print()
{
  T obj;
  obj.PrintStartMessage();

  return obj;
}

int main()
{
    Derived derived = construct_and_print<Derived>();
}
16
greatwolf

基本クラスコンストラクター内から派生仮想関数を呼び出すことができないため、想像どおりにそれを行うことはできません。オブジェクトはまだ派生型ではありません。しかし、これを行う必要はありません。

MyBaseの構築後にPrintStartMessageを呼び出す

次のようなことをしたいとしましょう:

class MyBase {
public:
    virtual void PrintStartMessage() = 0;
    MyBase() {
        printf("Doing MyBase initialization...\n");
        PrintStartMessage(); // ⚠ UB: pure virtual function call ⚠
    }
};

class Derived : public MyBase {
public:
    virtual void PrintStartMessage() { printf("Starting Derived!\n"); }
};

つまり、必要な出力は次のとおりです。

Doing MyBase initialization...
Starting Derived!

しかし、これがまさにコンストラクターの目的です!仮想関数をスクラップしてDerivedのコンストラクターにジョブを実行させるだけです。

class MyBase {
public:
    MyBase() { printf("Doing MyBase initialization...\n"); }
};

class Derived : public MyBase {
public:
    Derived() { printf("Starting Derived!\n"); }
};

出力は、まあ、私たちが期待するものです:

Doing MyBase initialization...
Starting Derived!

ただし、これは、派生クラスがPrintStartMessage機能を明示的に実装することを強制しません。しかし、その一方で、いずれにしても空の実装を常に提供できるため、それが必要かどうかをよく考えてください。

MyBaseの構築前にPrintStartMessageを呼び出す

上記のように、PrintStartMessageが構築される前にDerivedを呼び出したい場合、DerivedPrintStartMessageオブジェクトがまだないため、これを実行できません。 _呼び出される。 PrintStartMessageデータメンバーのいずれにもアクセスできないため、Derivedが非静的メンバーであることを要求しても意味がありません。

ファクトリ関数を持つ静的関数

または、次のように静的メンバーにすることもできます。

class MyBase {
public:
    MyBase() {
        printf("Doing MyBase initialization...\n");
    }
};

class Derived : public MyBase {
public:
    static void PrintStartMessage() { printf("Derived specific message.\n"); }
};

それがどのように呼ばれるかについて自然な疑問が生じますか?

私が見ることができる2つの解決策があります。1つは@greatwolfの解決策に似ており、手動で呼び出す必要があります。しかし、これは静的メンバーなので、MyBaseのインスタンスが構築される前に呼び出すことができます。

template<class T>
T print_and_construct() {
    T::PrintStartMessage();
    return T();
}

int main() {
    Derived derived = print_and_construct<Derived>();
}

出力は

Derived specific message.
Doing MyBase initialization...

このアプローチでは、すべての派生クラスがPrintStartMessageを実装する必要があります。残念ながら、ファクトリ関数でそれらを構築する場合にのみ当てはまります...これは、このソリューションの大きな欠点です。

2番目の解決策は、不思議な繰り返しのテンプレートパターン(CRTP)に頼ることです。コンパイル時に完全なオブジェクトタイプをMyBaseに通知することにより、コンストラクター内から呼び出しを実行できます。

template<class T>
class MyBase {
public:
    MyBase() {
        T::PrintStartMessage();
        printf("Doing MyBase initialization...\n");
    }
};

class Derived : public MyBase<Derived> {
public:
    static void PrintStartMessage() { printf("Derived specific message.\n"); }
};

専用のファクトリー関数を使用する必要なく、出力は期待どおりです。

CRTPを使用してPrintStartMessage内からMyBaseにアクセスする

MyBaseが実行されている間、そのメンバーにアクセスすることはすでにOKです。それを呼び出したPrintStartMessageMyBaseがアクセスできるようにすることができます:

template<class T>
class MyBase {
public:
    MyBase() {
        T::PrintStartMessage(this);
        printf("Doing MyBase initialization...\n");
    }
};

class Derived : public MyBase<Derived> {
public:
    static void PrintStartMessage(MyBase<Derived> *p) {
        // We can access p here
        printf("Derived specific message.\n");
    }
};

以下も有効であり、非常に頻繁に使用されますが、少し危険です。

template<class T>
class MyBase {
public:
    MyBase() {
        static_cast<T*>(this)->PrintStartMessage();
        printf("Doing MyBase initialization...\n");
    }
};

class Derived : public MyBase<Derived> {
public:
    void PrintStartMessage() {
        // We can access *this member functions here, but only those from MyBase
        // or those of Derived who follow this same restriction. I.e. no
        // Derived data members access as they have not yet been constructed.
        printf("Derived specific message.\n");
    }
};

テンプレートソリューションなし-再設計

さらに別のオプションは、コードを少し再設計することです。 IMOこれは、オーバーライドされたPrintStartMessageMyBase構造内から絶対に呼び出す必要がある場合に、実際に推奨されるソリューションです。

この提案は、次のようにDerivedMyBaseから分離することです。

class ICanPrintStartMessage {
public:
    virtual ~ICanPrintStartMessage() {}
    virtual void PrintStartMessage() = 0;
};

class MyBase {
public:
    MyBase(ICanPrintStartMessage *p) : _p(p) {
        _p->PrintStartMessage();
        printf("Doing MyBase initialization...\n");
    }

    ICanPrintStartMessage *_p;
};

class Derived : public ICanPrintStartMessage {
public:
    virtual void PrintStartMessage() { printf("Starting Derived!!!\n"); }
};

MyBaseは次のように初期化します。

int main() {
    Derived d;
    MyBase b(&d);
}
9
ybungalobill

コンストラクタでvirtual関数を呼び出さないでください。 期間PrintStartMessageを非virtualにして、すべてのコンストラクターで明示的に呼び出しを行うなど、いくつかの回避策を見つける必要があります。

6
Fred Foo

これは古い質問であることはわかっていますが、プログラムの作業中に同じ質問に遭遇しました。

目的が、Baseクラスに共有初期化コードを処理させることによってコードの重複を減らす一方で、派生クラスに純粋な仮想メソッドでそれらに固有のコードを指定することを要求する場合、これを決定しました。

#include <iostream>

class MyBase
{
public:
    virtual void UniqueCode() = 0;
    MyBase() {};
    void init(MyBase & other)
    {
      std::cout << "Shared Code before the unique code" << std::endl;
      other.UniqueCode();
      std::cout << "Shared Code after the unique code" << std::endl << std::endl;
    }
};

class FirstDerived : public MyBase
{
public:
    FirstDerived() : MyBase() { init(*this); };
    void  UniqueCode()
    {
      std::cout << "Code Unique to First Derived Class" << std::endl;
    }
private:
    using MyBase::init;
};

class SecondDerived : public MyBase
{
public:
    SecondDerived() : MyBase() { init(*this); };
    void  UniqueCode()
    {
      std::cout << "Code Unique to Second Derived Class" << std::endl;
    }
private:
    using MyBase::init;
};

int main()
{
    FirstDerived first;
    SecondDerived second;
}

出力は次のとおりです。

 Shared Code before the unique code
 Code Unique to First Derived Class
 Shared Code after the unique code

 Shared Code before the unique code
 Code Unique to Second Derived Class
 Shared Code after the unique code
1
Circuitrinos

PrintStartMessage()が純粋な仮想関数ではなく通常の仮想関数である場合、コンパイラはそれについて文句を言わないでしょう。ただし、PrintStartMessage()の派生バージョンが呼び出されない理由を理解する必要があります。

派生クラスは自身のコンストラクターの前に基本クラスのコンストラクターを呼び出すため、派生クラスは基本クラスのように動作し、基本クラスの関数を呼び出します。

1
fatma.ekici

テンプレートではなくMACROSを使用するか、純粋に言語の「自然な」制約内に留まるようにして、抽象基本クラスに回避策/「コンパニオン」を提供できます。

Init関数を使用して基本クラスを作成します。例:

class BaseClass
{
public:
    BaseClass(){}
    virtual ~BaseClass(){}
    virtual void virtualInit( const int i=0 )=0;
};

次に、コンストラクターのマクロを追加します。ここに複数のコンストラクター定義を追加しない理由や、複数のマクロから選択しない理由はありません。

#define BASECLASS_INT_CONSTRUCTOR( clazz ) \
    clazz( const int i ) \
    { \
        virtualInit( i ); \
    } 

最後に、派生にマクロを追加します。

class DervivedClass : public BaseClass
{
public:
    DervivedClass();
    BASECLASS_INT_CONSTRUCTOR( DervivedClass )
    virtual ~DervivedClass();

    void virtualInit( const int i=0 )
    {
        x_=i;
    }

    int x_;
};
0
BuvinJ