【问题标题】:Virtual methods in constructor构造函数中的虚方法
【发布时间】:2013-01-31 07:49:01
【问题描述】:

禁止在 C++ 中调用虚方法,但在某些情况下它可能非常有用。

考虑以下情况 - 一对 Parent 和 Child 类。父构造函数需要实例化一个子类,因为它必须以特殊方式对其进行初始化。 Parent 和 Child 都可以派生,因此 DerivedParent 使用 DerivedChild。

但是有一个问题 - 因为在从 DerivedParent::ctor 调用 Parent::ctor 时,基类应该有一个 DerivedChild 的实例而不是 Child。但这需要以某种方式调用虚拟方法,这是被禁止的。我说的是这样的事情:

class Child 
{ 
public:
    virtual std::string ToString() { return "Child"; }
};

class DerivedChild : public Child 
{
public:
    std::string ToString() { return "DerivedChild"; }
};

class Parent
{
protected:
    Child * child;

    virtual Child * CreateChild() { return new Child(); }

public:
    Parent() { child = CreateChild(); }
    Child * GetChild() { return child; }
};

class DerivedParent : public Parent
{
protected:
    Child * CreateChild() { return new DerivedChild(); }
};

int main(int argc, char * argv[])
{
    DerivedParent parent;

    printf("%s\n", parent.GetChild()->ToString().c_str());

    getchar();
    return 0;
}

让我们举一个真实的例子。假设我想为 WinApi 的窗口编写包装器。基本 Control 类应该注册类并实例化一个窗口(例如 RegisterClassEx 和 CreateWindowEx),以正确设置它(例如以这种方式注册类,该窗口结构具有类实例的附加数据;为所有控件设置通用 WndProc ; 通过 SetWindowLongPtr 等引用this

另一方面,派生类应该能够指定样式和扩展样式、窗口的类名等。

如果在 Control 的构造函数中构造窗口实例是要履行的合同,我认为除了在 ctor 中使用 VM 之外没有其他解决方案(这是行不通的)。

可能的解决方法:

  • 使用静态多态性(例如,类 Derived : public Base),但如果想要从 Derived 派生,它将不起作用;
  • 如果可能,将派生的 lambda 传递给基础 ctor - 它会起作用,但它是一个完全的核心解决方案;
  • 将大量参数从派生的 ctor 传递到基 ctor - 它应该可以工作,但既不优雅,也不易于使用。

我个人不喜欢他们俩。出于好奇,我检查了问题是如何在 Delphi 的 VCL 中解决的,你知道什么,基类调用 CreateParams,它是虚拟的(Delphi 允许这样的调用和保证,它们是安全的 - 类字段在创建时初始化为 0 )。

如何克服这种语言限制?


编辑:回应答案:

从派生的构造函数调用 CreateChild。您已经需要定义 CreateChild,因此这是一个增量步骤。您可以在基类中添加 protected: void init() 函数来封装此类初始化。

它会起作用,但它不是一个选项 - 引用著名的 C++ 常见问题解答:

第一个变体最初是最简单的,尽管实际上想要创建对象的代码需要一点程序员自律,这实际上意味着你注定要失败。说真的,如果只有一两个地方真正创建了这个层次的对象,程序员的自律是相当本地化的,应该不会造成问题。

使用 CRTP。将基类设为模板,让子类提供 DerivedType,然后以这种方式调用构造函数。这种设计有时可以完全消除虚函数。

这不是一个选项,因为它只能工作一次,用于基类及其直接后代。

使子指针成为构造函数参数,例如工厂函数。

到目前为止,这是最好的解决方案 - 将代码注入基本 ctor。就我而言,我什至不需要这样做,因为我可以参数化基本 ctor 并从后代传递值。但它确实会起作用。

使用工厂函数模板在返回父级之前生成适当类型的子级指针。这消除了类模板的复杂性。使用类型特征模式对子类和父类进行分组。

是的,但它有一些缺点:

  • 从我的课程派生的某人可能会发布他的 ctor 并绕过安全措施;
  • 它会阻止使用引用,必须使用智能指针。

【问题讨论】:

  • 它们不被禁止。它们只会让你的生活更加悲惨。
  • A 不完全同意悲惨的部分。首先,在给出的示例中,它们似乎是解决问题的唯一优雅方法。此外,如果虚拟方法符合静态(不使用类字段)的条件,那么调用这样的方法是绝对安全的。
  • 关于禁止部分。我将示例修改为可编译的。运行后,它显示的是“Child”而不是“DerivedChild”,因此虚拟方法(虚拟,例如 DerivedParent::CreateChild())没有被调用。
  • 这里有可以回答的问题吗? (问题通常以问号结尾?
  • @abelenky 我已经收到了我的问题的答案,但我已经编辑了帖子,现在它包含了一个明确的问题。

标签: c++ constructor virtual-functions


【解决方案1】:

禁止在 C++ 中调用虚方法,但在某些情况下它可能非常有用。

不,在构造函数中调用虚方法会分派给派生最多的完整对象,也就是正在构建的对象。它不是被禁止的,它是明确定义的,它会做唯一有意义的事情。

不允许 C++ 基类知道最派生对象的身份,无论好坏。在该模型下,派生对象直到基础构造函数运行后才开始存在,因此无需获取类型信息。

在您的情况下,一些替代方案是:

  1. 从派生构造函数调用 CreateChild。您已经要求定义 CreateChild,所以这是一个增量步骤。您可以在基类中添加protected: void init() 函数来封装此类初始化。
  2. 使用 CRTP。将基类设为模板,让子类提供 DerivedType,然后以这种方式调用构造函数。这种设计有时可以完全消除虚函数。
  3. 使子指针成为构造函数参数,例如工厂函数。
  4. 使用工厂函数模板在返回父对象之前生成适当类型的子指针。这消除了类模板的复杂性。使用类型特征模式对子类和父类进行分组。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-09-02
    • 2018-02-09
    • 1970-01-01
    • 2023-03-27
    • 1970-01-01
    • 2010-09-09
    • 1970-01-01
    相关资源
    最近更新 更多