51

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

void PrintStartMessage() = 0

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

次に、それを base class( 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 

どうすればいいですか?

4

8 に答える 8

43

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

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

于 2011-12-25T15:23:13.147 に答える
23

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

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

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

  return obj;
}

int main()
{
    Derived derived = construct_and_print<Derived>();
}
于 2011-12-25T15:50:40.777 に答える
13

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

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まだないため、これを行うことはできません。どのデータ メンバーにもアクセスできないため、非静的メンバーであることを要求しても意味がありません。DerivedPrintStartMessagePrintStartMessageDerived

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

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

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 番目の解決策は、Curiously Recurring Template Pattern (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です。PrintStartMessageそれを呼び出した にアクセスできるようにすることができますMyBase:

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");
    }
};

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

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

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

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);
}
于 2011-12-25T16:11:00.893 に答える
6

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

于 2011-12-25T15:16:13.783 に答える
1

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

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

于 2012-12-27T14:00:55.537 に答える
0

同じ問題に直面して、私は(完全ではない)解決策を想像しました。アイデアは、純粋な仮想初期化関数が構築後に呼び出されるという証明書を基本クラスに提供することです。

class A
{
  private:
    static const int checkValue;
  public:
    A(int certificate);
    A(const A& a);
    virtual ~A();
    virtual void init() = 0;
  public:
    template <typename T> static T create();
    template <typeneme T> static T* create_p();
    template <typename T, typename U1> static T create(const U1& u1);
    template <typename T, typename U1> static T* create_p(const U1& u1);
    //... all the required possibilities can be generated by prepro loops
};

const int A::checkValue = 159736482; // or any random value

A::A(int certificate)
{
  assert(certificate == A::checkValue);
}

A::A(const A& a)
{}

A::~A()
{}

template <typename T>
T A::create()
{
  T t(A::checkValue);
  t.init();
  return t;
}

template <typename T>
T* A::create_p()
{
  T* t = new T(A::checkValue);
  t->init();
  return t;
}

template <typename T, typename U1>
T A::create(const U1& u1)
{
  T t(A::checkValue, u1);
  t.init();
  return t;
}

template <typename T, typename U1>
T* A::create_p(const U1& u1)
{
  T* t = new T(A::checkValue, u1);
  t->init();
  return t;
}

class B : public A
{
  public:
    B(int certificate);
    B(const B& b);
    virtual ~B();
    virtual void init();
};

B::B(int certificate) :
  A(certificate)
{}

B::B(const B& b) :
  A(b)
{}

B::~B()
{}

void B::init()
{
  std::cout << "call B::init()" << std::endl;
}

class C : public A
{
  public:
    C(int certificate, double x);
    C(const C& c);
    virtual ~C();
    virtual void init();
  private:
    double x_;
};

C::C(int certificate, double x) :
  A(certificate)
  x_(x)
{}

C::C(const C& c) :
  A(c)
  x_(c.x_)
{}

C::~C()
{}

void C::init()
{
  std::cout << "call C::init()" << std::endl;
}

次に、クラスのユーザーは証明書を提供しないとインスタンスを構築できませんが、証明書は作成関数によってのみ生成できます。

B b = create<B>(); // B::init is called
C c = create<C,double>(3.1415926535); // C::init is called

さらに、ユーザーは、コンストラクターで証明書の送信を実装しないと、AB または C を継承する新しいクラスを作成できません。次に、基本クラス A には、構築後に init が呼び出されるという保証があります。

于 2020-06-03T05:51:03.303 に答える