51

我有一个包含纯虚函数的基类 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 

我必须怎么做?

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,则无法完成此操作,因为还没有可以调用的Derived对象。PrintStartMessage要求PrintStartMessage成为非静态成员是没有意义的,因为它无法访问任何Derived数据成员。

带有工厂函数的静态函数

或者,我们可以将其设为静态成员,如下所示:

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

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

一个自然的问题是如何称呼它?

我可以看到两种解决方案:一种类似于@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。不幸的是,只有当我们用我们的工厂函数构造它们时它才是真的……这是这个解决方案的一个巨大缺点。

第二种解决方案是求助于奇怪重复模板模式 (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执行时,已经可以访问其成员了。我们可以使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");
    }
};

无模板解决方案——重新设计

另一种选择是稍微重新设计您的代码。PrintStartMessage如果您绝对必须从MyBase构造中调用重写,则 IMO 这实际上是首选解决方案。

本提案与 分开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);
}
于 2011-12-25T16:11:00.893 回答
6

virtual您不应该在构造函数中调用函数。期间。您必须找到一些解决方法,例如在每个构造函数中显式调用PrintStartMessagenon-和调用。virtual

于 2011-12-25T15:16:13.783 回答
1

如果 PrintStartMessage() 不是纯虚函数而是普通虚函数,编译器不会抱怨。但是,您仍然需要弄清楚为什么没有调用 PrintStartMessage() 的派生版本。

由于派生类在其自己的构造函数之前调用基类的构造函数,因此派生类的行为类似于基类,因此调用基类的函数。

于 2012-12-27T14:00:55.537 回答
0

我知道这是一个老问题,但我在编写程序时遇到了同样的问题。

如果您的目标是通过让基类处理共享初始化代码来减少代码重复,同时要求派生类在纯虚拟方法中指定对它们唯一的代码,这就是我决定的。

#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
于 2018-10-05T23:47:03.320 回答
0

面对同样的问题,我想出了一个(不完美的)解决方案。这个想法是向基类提供一个证书,在构造之后将调用纯虚拟 init 函数。

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 回答
-1

我可以使用 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_;
};
于 2020-02-15T00:50:55.267 回答