从基类构造函数调用纯虚函数

Posted

技术标签:

【中文标题】从基类构造函数调用纯虚函数【英文标题】:call to pure virtual function from base class constructor 【发布时间】:2012-01-27 15:30:24 【问题描述】:

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

我必须怎么做?

【问题讨论】:

@peachykeen 没有构造函数的类可以做什么?你无法建造它! @peachykeen "你可以从中派生。" 是的。但是既然不能构造它,也不能构造派生类的任何实例。 @peachykeen 当然不是。谁跟你说这些废话的?没有基类构造函数就无法创建派生实例。 @peachykeen "你绝对可以构造派生类," 你不能构造派生类,因为你的基类缺少任何构造函数(假设)。为了构造一个派生实例你需要先构造一个基础实例。 让我们continue this discussion in chat 【参考方案1】:

有很多文章解释了为什么你不应该在 C++ 的构造函数和析构函数中调用虚函数。请查看here 和here,了解此类通话期间幕后发生的详细信息。

简而言之,对象是从基础构造到派生的。因此,当您尝试从基类构造函数调用虚函数时,尚未发生从派生类的覆盖,因为尚未调用派生构造函数。

【讨论】:

如果基构造函数调用了调用虚函数的非虚函数怎么办? @shadow_map 哪个函数调用虚函数并不重要。【参考方案2】:

在该对象仍在构造时尝试从派生中调用纯抽象方法是不安全的。这就像试图给汽车加气,但那辆汽车还在装配线上,油箱还没有放进去。

你能做的最接近的事情是先完全构造你的对象,然后再调用方法:

template <typename T>
T construct_and_print()

  T obj;
  obj.PrintStartMessage();

  return obj;


int main()

    Derived derived = construct_and_print<Derived>();

【讨论】:

"这就像给汽车加气,但那辆车还在装配线上,油箱还没有放好。"太好了! 就个人而言,我希望能够使用由子类配置的值来集中一个通用的初始化序列,这是其他语言中的常见模式,如 Obj-C、Ruby、Python(我知道是动态的) “我的意思是你到底希望发生什么?”这是一个严重的问题吗 ?在执行初始化列表之前,内存已经分配,​​并且在构造函数体中没有明显的迹象表明虚函数表是否已初始化。实际上,如果它已经在构造函数主体中初始化,那将是有意义的。我看不出有什么理由可以调用某些方法而某些方法此时不能调用,尤其是因为非虚拟可以调用纯虚拟。 @Virus721 “没有明显的事情表明虚函数表在构造函数体中是否已初始化。”是的,有:C++ 标准。 Vtables 是在构建每个派生层时构建的,句号。你认为它是否有意义并不重要! 太棒了!对象创建后初始化的事情,其实让我想起了这种情况下可以使用的工厂模式。【参考方案3】:

你不能按照你想象的方式去做,因为你不能从基类构造函数中调用派生虚函数——对象还不是派生类型。但你不需要这样做。

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

如上所述,如果您想在构造 Derived 之前调用 PrintStartMessage,则无法完成此操作,因为尚未调用 PrintStartMessageDerived 对象。要求 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");
    
;

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

另一种选择是稍微重新设计您的代码。如果您绝对必须从 MyBase 构造中调用覆盖的 PrintStartMessage,则 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);

【讨论】:

【参考方案4】:

您不应该在构造函数中调用virtual 函数。 Period。您必须找到一些解决方法,例如将PrintStartMessage 设为非virtual 并将调用显式地放入每个构造函数中。

【讨论】:

我想对所有派生类执行它,并在顾问中调用它我该怎么做? @herzlshemuelian 就像他说的:不是! 为了更清楚一点,也可以从构造函数或析构函数调用虚函数,只是不会导致调用函数的派生类版本。构造函数和析构函数中的this 始终是其构造函数或析构函数被调用的类的类型,因此动态调度导致调用基类版本的覆盖函数。 @Als 在构造函数中调用纯虚函数是未定义的行为。 @fefe:是的,你是对的,C++03 10.4/6 声明 “可以从抽象的构造函数(或析构函数)调用成员函数类;对于从这样的构造函数(或析构函数)创建(或销毁)的对象,直接或间接地对纯虚函数进行虚调用(10.3)的效​​果是未定义的。”【参考方案5】:

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

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

【讨论】:

这都是真的而且写得很好,但是因为它是通过将焦点切换到非纯虚拟方法来打开的,所以它没有回答给定的问题,也不需要发布一年稍后。【参考方案6】:

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

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

#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

【讨论】:

错误的解决方案。如果您从FirstDerived 类派生ThirdDerived,则将调用FirstDerived::init 而不是ThirdDerived::init @Caduchon 对于我正在处理的应用程序,我只需要 1 的深度。如果您的用例需要更多,这只是一个“错误的解决方案”。【参考方案7】:

面对同样的问题,我想出了一个(不完美的)解决方案。这个想法是为基类提供一个证书,在构造之后将调用纯虚拟 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

此外,如果不在构造函数中实现证书传输,用户就无法创建从 A B 或 C 继承的新类。然后,基类 A 保证 init 将在构造后被调用。

【讨论】:

【参考方案8】:

我可以使用 MACROS 而不是模板为您的抽象基类提供解决方法/“伴侣”,或者完全遵守语言的“自然”约束。

使用初始化函数创建一个基类,例如:

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_;
;

【讨论】:

如果你创建另一个类Derived2 继承自DerivedClass,你会感到很意外:DerivedClass::virtualInit 会被调用,而不是Derived2::virtualInit 是的...您只需要再次为 DerivedClass2 包含宏。这不是一个“万无一失”的解决方案,也不适合在公共图书馆或其他地方使用,但它是私有实现的可行选择。一般来说,宏总是倾向于这样。

以上是关于从基类构造函数调用纯虚函数的主要内容,如果未能解决你的问题,请参考以下文章

如何从基类调用派生赋值运算符?

虚函数和基类中的this指针的问题!

尝试从基类访问受保护的构造函数时获取错误C2248

虚析构函数与纯虚函数

构造函数和析构函数

虚函数构造和析构函数执行顺序总结