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

her*_*ian 35 c++ constructor abstract-class pure-virtual object-lifetime

我有一个包含纯虚函数的基类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;
 }
Run Code Online (Sandbox Code Playgroud)

但我收到链接器错误.

 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)
Run Code Online (Sandbox Code Playgroud)

我想强制所有派生类来...

A- implement it

B- call it in their constructor 
Run Code Online (Sandbox Code Playgroud)

我该怎么做?

a1e*_*x07 33

有很多文章解释了为什么你永远不应该在C++中的构造函数和析构函数中调用虚函数.看看这里这里详细了解这些调用过程中幕后发生的事情.

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

  • 两个链接都已死,提供直接答案的原因是优先的. (18认同)
  • 如果基础构造函数调用调用虚函数的非虚函数,该怎么办? (3认同)
  • @shadow_map 哪个函数调用虚函数并不重要。 (3认同)

gre*_*olf 16

在该对象仍在构造时尝试从派生中调用纯抽象方法是不安全的.这就像试图将汽油填充到汽车中,但该汽车仍在装配线上并且尚未放入油箱.

你可以做的最接近的事情就是首先完全构造你的对象,然后在之后调用方法:

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

  return obj;
}

int main()
{
    Derived derived = construct_and_print<Derived>();
}
Run Code Online (Sandbox Code Playgroud)

  • "_这就像试图将汽油加入汽车,但那辆汽车仍在装配线上,而且油箱尚未放入."非常好! (15认同)
  • @Virus721“没有明显的事情表明虚拟函数表在构造函数体内是否已初始化。” 是的,有:C++ 标准。Vtables 是在构建每个派生层时构建的,句号。你认为有没有道理并不重要! (2认同)

ybu*_*ill 9

您无法以您想象的方式执行此操作,因为您无法从基类构造函数中调用派生的虚函数 - 该对象尚未属于派生类型.但你不需要这样做.

在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"); }
};
Run Code Online (Sandbox Code Playgroud)

所需的执行跟踪将是:

Doing MyBase initialization...
Starting Derived!
Run Code Online (Sandbox Code Playgroud)

但这就是构造函数的用途!只是废弃虚函数并使派生的构造函数完成工作!

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

class Derived : public MyBase {
public:
    Derived() { printf("Starting Derived!\n"); }
};
Run Code Online (Sandbox Code Playgroud)

输出就是我们所期望的:

Doing MyBase initialization...
Starting Derived!
Run Code Online (Sandbox Code Playgroud)

但是,这并不强制派生类显式实现该Derived功能.但另一方面,请三思而后行是否有必要,因为无论如何它们总是可以提供空的实现.

在构建MyBase之前调用PrintStartMessage

如上所述,如果要在构造函数PrintStartMessage之前调用PrintStartMessage,则无法完成此操作,因为还没有要调用的Derived对象Derived.要求PrintStartMessage成为非静态成员是没有意义的,因为它无法访问任何PrintStartMessage数据成员.

具有工厂功能的静态功能

或者,我们可以使它像这样的静态成员:

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

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

一个自然的问题是它将如何被称为?

我可以看到两种解决方案:一种类似于@greatwolf,你必须手动调用它.但是现在,因为它是一个静态成员,所以你可以Derived在构造一个实例之前调用它:

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

int main() {
    Derived derived = print_and_construct<Derived>();
}
Run Code Online (Sandbox Code Playgroud)

输出将是

Derived specific message.
Doing MyBase initialization...
Run Code Online (Sandbox Code Playgroud)

这种方法确实强制实现所有派生类MyBase.不幸的是,只有当我们用我们的工厂功能构建它们时才会这样......这是这个解决方案的一个巨大缺点.

第二种解决方案是采用奇怪的重复模板模式(CRTP).通过PrintStartMessage在编译时告诉完整的对象类型,它可以在构造函数中进行调用:

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"); }
};
Run Code Online (Sandbox Code Playgroud)

输出符合预期,无需使用专用工厂功能.

使用CRTP从PrintStartMessage中访问MyBase

MyBase执行时,它已经可以访问其成员.我们可以MyBase访问PrintStartMessage调用它的那个:

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");
    }
};
Run Code Online (Sandbox Code Playgroud)

以下也是有效且经常使用的,虽然有点危险:

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");
    }
};
Run Code Online (Sandbox Code Playgroud)

没有模板解决方案 - 重新设计

还有一种选择是重新设计你的代码.IMO这个实际上是首选的解决方案,如果你绝对必须MyBasePrintStartMessage构造中调用一个被覆盖的.

这个建议是分开MyBaseDerived,如下:

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"); }
};
Run Code Online (Sandbox Code Playgroud)

您初始化MyBase如下:

int main() {
    Derived d;
    MyBase b(&d);
}
Run Code Online (Sandbox Code Playgroud)

  • 不知道为什么它会被投票...特别是因为它完全是OP点.建设性的批评是受欢迎的.更新它以便更容易说明问题. (2认同)

Fre*_*Foo 6

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

  • @herzlshemuelian:你做不到. (2认同)
  • 为了使它更清楚,可以从构造函数或析构函数调用虚函数,只是它不会导致调用函数的派生类版本.构造函数和析构函数中的`this`始终是调用其构造函数或析构函数的类的类型,因此动态调度会导致调用覆盖函数的基类版本. (2认同)
  • @fefe:是的,你是对的,**C++03 10.4/6** 声明 *“可以从抽象类的构造函数(或析构函数)调用成员函数;进行虚拟调用(10.3)的效​​果直接或间接用于从此类构造函数(或析构函数)创建(或销毁)的对象的纯虚函数是未定义的。”* (2认同)