【问题标题】:Virtual Function During Construction Workaround构建过程中的虚拟功能解决方法
【发布时间】:2014-06-11 17:57:33
【问题描述】:

我有一个具有虚函数的基类。我想在构造过程中调用该类,因为我希望为每个派生类调用该函数。我知道在构造过程中我不能调用虚函数,但我想不出一个优雅的(即避免重复代码)解决方案。

在构造过程中调用虚函数有哪些变通方法?

我想避免这种情况的原因是我不想创建只调用基类的构造函数。

class A {
    public:
        A() {
            read();
        }

        // This never needs to be called
        virtual void read() = 0;
}

class B:A {
    public:
        B():A() {   };
        read() { /*Do something special for B here.*/ }

}

class C:A {
    public:
        C():A() {   };
        read() { /*Do something special for C here.*/ }

}

PS:Python 的做法很简单,就是在A::read() 中输入raise NotImplementedError。我正在回归 C++,但我比我想象的还要生疏。

【问题讨论】:

  • 你对课堂布局有什么限制?至少部分自动化的一种方法是将 CRTP 类添加到您的类层次结构中。
  • 到目前为止,我没有太多限制。我以前从未听说过 CRTP。我去看看。
  • 你可以在构造过程中调用虚函数,但它会是你所在的构造函数的类的实现。
  • ??那么,您想在构造派生之前在基类中调用属于派生类的函数吗?馊主意。或者你想调用一个后构造器,在这种情况下解决方案是工厂方法?
  • 很好地讨论了解决此类问题的几种方法:isocpp.org/wiki/faq/…

标签: c++ constructor virtual


【解决方案1】:

这是工厂方法的做法,把工厂放到基类中:

class A {
public:
    virtual void read() = 0;
    template<class X> static X* create() {X* r = new X;X->read();return X;}
    virtual A* clone() const = 0;
};

class B : public A {
    B():A() {   };
    friend class A;
public:
    void read() { /*Do something special for B here.*/ }
    B* clone() const {return new B(*this);}
};

class C : public A {
    C():A() {   };
    friend class A;
public:
    void read() { /*Do something special for C here.*/ }
    C* clone() const {return new C(*this);}
};

添加了一个带有协变返回类型的clone-方法作为奖励。

使用CRTP:

class A {
public:
    // This never needs to be called
    virtual void read() = 0;
    virtual A* clone() const = 0;
};
template<class D, class B> struct CRTP : B {
    D* clone() {return new D(*this);}
    static D* create() {return new D();}
};

class B : public CRTP<B, A> {
    B() {   };
public:
    void read() { /*Do something special for B here.*/ }
};

class C : public CRTP<C, A> {
    C() {   };
public:
    void read() { /*Do something special for C here.*/ }
};

【讨论】:

  • 我不确定我明白了。我是先实例化一个BC 对象,然后再调用clone() 吗?
  • 对于克隆,您需要一个要克隆的对象。要创建Y,请调用X::create&lt;Y&gt;()X 是类A 或派生类,而不是对象)
  • 那么,这些 ctor 不应该受到保护或私有化吗?
  • @paulm:它们是,因为它们在任何其他访问说明符之前都在一个类中。
  • 啊,我自动解析:public CRTP as public:
【解决方案2】:

FAQ 视角。

这是一个常见问题。

请参阅标题为 “Okay, but is there a way to simulate that behavior as if dynamic binding worked on the this object within my base class's constructor?” 的 C++ 常见问题解答项目。

在询问之前检查常见问题解答(通常是谷歌搜索或altavista'ing)通常是个好主意。


问题是“派生类特定的基础初始化”。

要清楚,而上面的字面问题是

“在构造过程中调用虚函数有哪些变通方法?”

很明显是什么意思

“如何设计基类B,以便每个派生类都可以指定B 构造过程中发生的部分情况?”

一个主要的例子是 C 风格的 GUI 功能被 C++ 类包装。然后,一般的Widget 构造函数可能需要实例化一个 API 级小部件,根据派生程度最高的类,该小部件应该是按钮小部件或列表框小部件或其他任何东西。所以派生最多的类必须以某种方式影响Widget 的构造函数中发生的事情。

换句话说,我们谈论的是派生类特定的基础构造

Marshall Cline 称之为“构造过程中的动态绑定”,它在 C++ 中存在问题,因为在 C++ 中,类T 构造和销毁期间对象的动态类型是T。这有助于类型安全,因为在子对象已初始化或其初始化已开始之前,不会在派生类子对象上调用虚拟成员函数。但一个主要成本是 DBDI(显然)不能以既简单又安全的方式完成。


可以执行派生类特定初始化的位置。

在问题中,派生类特定操作称为read。这里我称之为derived_action。调用 derived_action 的位置有 3 种主要可能性:

  • 由实例化代码调用,称为两阶段构造
    这本质上意味着手头有一个几乎不可用的未完全初始化的对象,一个僵尸对象。然而,随着 C++11 移动语义变得更加普遍和被接受(无论如何它可以通过使用工厂在一定程度上得到缓解)。一个主要问题是,在构造的第二阶段,由于构造过程中的动态类型更改,不存在针对未初始化子对象的虚拟调用的普通 C++ 保护。

  • Derived 构造函数调用。
    例如,derived_action 可以作为Base 构造函数的参数表达式调用。一种并非完全不常见的技术是使用类模板来生成大多数派生类,例如提供derived_action的电话。

  • Base 构造函数调用。
    这意味着derived_action 的知识必须动态或静态地传递给构造函数。一个不错的方法是使用默认的构造函数参数。这导致了并行类层次结构的概念,即派生类操作的层次结构。

此列表按复杂程度和类型安全性的顺序排列,并且据我所知,它还反映了各种技术的历史用途。

例如在 Microsoft 的 MFC 和 Borland 的 ObjectWindows GUI 1990 年早期的库中,两阶段构建很常见,而这种设计现在,截至 2014 年,被认为是非常糟糕的。

【讨论】:

  • 感谢您的回复。我看过那个FAQ,但起初并不太明白。多读几遍后,我想我开始弄明白了。
【解决方案3】:

解决方法是构造之后调用虚函数。然后,您可以在工厂函数中耦合这两个操作(构造 + 虚拟调用)。基本思路如下:

class FactoryA
{
public:
    A *MakeA() const
    {
        A *ptr = CreateA();
        ptr->read();
        return ptr;
    }

    virtual ~FactoryA() {}

private:
    virtual A *CreateA() const = 0;
};

class FactoryB : public FactoryA
{
private:
    virtual A *CreateA() const { return new B; }
};

// client code:

void f(FactoryA &factory)
{
    A *ptr = factory.MakeA();
}

【讨论】:

  • 补充:Final 类可以将init 的调用放入它们的构造函数中。
  • @Deduplicator:你能详细说明一下吗?我认为这会打破这个概念,因为调用会发生两次(首先在构造函数中,然后在工厂函数中)。
  • 当你有一个叶子类时,构造函数可以工厂函数。
  • 对不起,我不明白。你是指产品类的构造函数还是工厂类的构造函数?
  • PS:很明显,当一个类是最终类时,您不必担心子类会覆盖您的虚函数实现,但我看不出这与工厂方法如何兼容。
【解决方案4】:

一种方法可以实现这一点,只需将其委托给另一个类(可能是朋友),并且可以确保在完全构造时被调用。

class A
{
friend class C;
private:
    C& _c; // this is the actual class!    
public:
    A(C& c) : _c(c) { };
    virtual ~A() { };
    virtual void read() = 0;
};


class B : public A
{
public:
    B(C& c) : A(c) { };
    virtual ~B() { };

    virtual void read() { 
       // actual implementation
    };
};


class C
{
private:
    std::unique_ptr<A> _a;

public:
    C() : _a(new B(*this)) { // looks dangerous?  not at this point...
        _a->read(); // safe now
    };
};

在这个例子中,我只是创建了一个B,但是你如何做取决于你想要实现的目标,如果需要的话,在 C 上使用模板,例如:

template<typename VIRTUAL>
class C 
{
private:
   using Ptr = std::unique_ptr<VIRTUAL>;

   Ptr _ptr;
public:
   C() : _ptr(new VIRTUAL(*this)) {
       _ptr->read();
   };
}; // eo class C

【讨论】:

    【解决方案5】:

    正如 Benjamin Bannier 所提到的,您可以使用 CRTP(一个定义实际 read() 函数的模板)。该方法的一个问题是模板必须始终内联编写。这有时可能会出现问题,尤其是在您要编写非常大的函数时。

    另一种方法是将函数指针传递给构造函数。虽然在某种程度上,它类似于在构造函数中调用函数,但它强制你传递一个指针(尽管在 C++ 中你总是可以传递 nullptr。)

    class A
    {
    public:
        A(func_t f)
        {
            // if(!f) throw ...;
            (*f)();
        }
    };
    
    class B : A
    {
    public:
        B() : A(read) {}
    
        void read() { ... }
    };
    

    显然,您在 read() 函数及其调用的任何函数中都存在“无法调用其他虚拟函数”的问题。另外, B 的变量成员尚未初始化。在这种情况下,这可能是一个最糟糕的问题......

    因此,这样写更安全:

       B() : A()
       {
           read();
       }
    

    但是,在这种情况下,这可能是您使用一些 for of init() 函数的时候。该 init() 函数可以在 A() 中实现(如果您使其可访问:即在派生时使用 public A)并且该函数可以按预期调用所有虚函数:

    class A
    {
    public:
        void init()
        {
            read();
        }
    };
    
    class B : public A
    {
    public:
        ...
    };
    

    我知道很多人说 init() 函数是邪恶的,因为创建 B 对象的人现在需要知道如何调用它……但您无能为力。话虽如此,您可以拥有一种工厂形式,并且该工厂可以根据需要进行 init() 调用。

    class B : public A
    {
    public:
        static B *create() { B *b(new B); b->init(); return b; }
    
    private:
        B() { ... } // prevent creation without calling create()
    };
    

    【讨论】:

    • 您真的提倡在基础对象上调用派生函数吗? 未定义的行为向您致意。
    • Deduplicator,我没有将读取函数标记为虚函数。
    • 在部分构造的对象中调用虚函数是安全的,只是不能调用纯虚函数,也不能传递未构造的子对象。但是在实际上并不在那个时刻派生的基础上调用派生函数总是未定义的行为,不管你怎么做。
    • 另外,真正的解决方案是我提出的最后一个代码示例,而不是第一个。如果他想“以错误的方式”做这件事,有解决办法。我也不会这样做,因为它根本不安全。
    • A(read) 无论如何都不会编译,除非B::read 是静态成员函数。
    猜你喜欢
    • 2019-06-14
    • 2012-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多