有没有自动化的方法来实现构造函数后和析构函数前的虚拟方法调用?

Posted

技术标签:

【中文标题】有没有自动化的方法来实现构造函数后和析构函数前的虚拟方法调用?【英文标题】:Is there any automated way to implement post-constructor and pre-destructor virtual method calls? 【发布时间】:2010-11-12 05:08:42 【问题描述】:

由于从构造函数和析构函数内部调用虚方法的众所周知的问题,我通常最终得到的类需要在构造函数之后调用最终设置方法,并调用预拆卸方法就在它们的析构函数之前,像这样:

MyObject * obj = new MyObject;
obj->Initialize();   // virtual method call, required after ctor for (obj) to run properly
[...]
obj->AboutToDelete();  // virtual method call, required before dtor for (obj) to clean up properly
delete obj;

这可行,但它带来了调用者在适当的时候忘记调用其中一个或两个方法的风险。

所以问题是:在 C++ 中有没有办法让这些方法被自动调用,这样调用者就不必记住调用它们了? (我猜没有,但我想我还是会问一下,以防万一有什么聪明的方法)

【问题讨论】:

析构函数有什么问题? 也许你应该描述你的实际问题,也许你实际上并不需要这些调用...... 如果你“通常”需要从 ctors 或 dtors 调用虚方法,听起来你有一个主要的设计问题。你能举一个必要的类的例子吗?最有可能的是,有一个更简单的解决方案。 (像往常一样,我希望 RAII 能够解决问题。将问题委托给一个或多个成员变量,他们自己的 ctors/dtors 各自进行初始化/拆卸。 示例:我有一个 Thread 类,用于管理它在内部保存的线程。用户子类化 Thread 类以提供他自己的入口点方法和成员变量供线程使用。目前,用户必须确保在删除线程对象之前调用 ShutdownInternalThread(),否则在调用子类的析构函数和调用 Thread 类的析构函数之间存在竞争条件,在此期间线程可能会尝试访问已经销毁的子类成员变量。我想删除 ShutdownInternalThread()。 示例:我有一个带有虚拟方法 GetWindowTypeName() 的 Window 类,子类必须实现该方法才能返回窗口的名称。我想确保调用 setWindowTitle(GetWindowTypeName()) 以在 Qt-land 中适当地设置窗口标题,但我不能在 Window 类 ctor 中这样做,因为虚拟方法调用在那里不起作用。所以它需要稍后在单独的方法调用中发生;但我不想强迫用户记住单独拨打电话。 (注意:这个例子有点做作;因为在 Qt 中我可以覆盖 showEvent()... 但你明白了) 【参考方案1】:

我使用了一个非常精心设计的Create() 工厂方法(每个类的静态成员)来调用构造函数和初始化程序对,其顺序与 C# 初始化类型的顺序相同。它返回一个shared_ptr 给该类型的一个实例,保证堆分配。随着时间的推移,它被证明是可靠且一致的。

诀窍:我从 XML 生成了我的 C++ 类声明...

【讨论】:

我假设您向shared_ptr 提供了一个自定义删除器,其中包括对预销毁逻辑的调用?【参考方案2】:

我能想到的最好方法是让您使用静态 Create 方法实现您自己的智能指针,该方法更新一个实例并调用 Initialize,并在其析构函数中调用 AboutToDelete 然后删除。

【讨论】:

【参考方案3】:

虽然没有自动化方法,但您可以通过拒绝用户访问该类型的析构函数并声明特殊的删除方法来强制用户手动操作。在这种方法中,您可以进行您想要的虚拟呼叫。创建可以采用与静态工厂方法类似的方法。

class MyObject 
  ...
public:
  static MyObject* Create()  
    MyObject* pObject = new MyObject();
    pObject->Initialize();
    return pObject;
  
  Delete() 
    this->AboutToDelete();
    delete this;
  
private:
  MyObject()  ... 
  virtual ~MyObject()  ... 
;

现在不能调用“delete obj;”除非调用站点有权访问 MyObject 私有成员。

【讨论】:

析构函数可以(也应该)是虚拟的,为什么要进行额外的工作? 这个例子不太正确,因为 MyObjects 不能(容易地)存储在标准容器(向量、列表等)中,而不是 Delete() 方法,覆盖类的删除运算符. 如果您希望 MyObject 是可派生的(我怀疑声明析构函数为虚拟),则应将构造函数和析构函数声明为受保护而不是私有【参考方案4】:

除了 JavedPar 的预销毁方法的想法外,在 C++ 中没有预制的解决方案可以轻松地进行两阶段构造/销毁。最明显的方法是遵循 C++ 中最常见的问题答案:“添加另一个间接层”。 您可以将此类层次结构的对象包装在另一个对象中。该对象的构造函数/析构函数然后可以调用这些方法。例如,查看 Couplien 的信封成语,或使用已经建议的智能指针方法。

【讨论】:

【参考方案5】:

http://www.research.att.com/~bs/wrapper.pdf Stroustrup 的这篇论文将解决您的问题。

我在 VS 2008 和 UBUNTU 上针对 g++ 编译器进行了测试。效果很好。

#include <iostream>

using namespace std;

template<class T>

class Wrap

    typedef int (T::*Method)();
    T* p;
    Method _m;
public:
    Wrap(T*pp, Method m): p(pp), _m(m)   (p->*_m)(); 
    ~Wrap()  delete p; 
;

class X

public:
    typedef int (*Method)();
    virtual int suffix()
    
        cout << "X::suffix\n";
        return 1;
    

    virtual void prefix()
    
        cout << "X::prefix\n"; 
    

    X()   cout << "X created\n"; 

    virtual ~X()  prefix(); cout << "X destroyed\n"; 

;

class Y : public X

public:
    Y() : X()  cout << "Y created\n"; 
    ~Y()  prefix(); cout << "Y destroyed\n"; 
    void prefix()
    
        cout << "Y::prefix\n"; 
    

    int suffix()
    
        cout << "Y::suffix\n";
        return  1;
    
;

int main()

    Wrap<X> xx(new X, &X::suffix);
    Wrap<X>yy(new Y, &X::suffix);

【讨论】:

+1 非常有趣的文章。然而,这似乎只包装标准方法而不是构造函数和析构函数。【参考方案6】:

在 C++ 中添加 post-constructors 的主要问题是,目前还没有人确定如何处理 post-post-constructors、post-post-post-constructors 等。

基本理论是对象具有不变量。这个不变量是由构造函数建立的。一旦建立,就可以调用该类的方法。随着需要后构造函数的设计的引入,您正在引入一旦构造函数运行后类不变量不会建立的情况。因此,允许从后构造函数调用虚函数同样不安全,并且您会立即失去它们似乎具有的一个明显好处。

正如您的示例所示(可能您没有意识到),它们不是必需的:

MyObject * obj = new MyObject;
obj->Initialize();   // virtual method call, required after ctor for (obj) to run properly

obj->AboutToDelete();  // virtual method call, required before dtor for (obj) to clean up properly
delete obj;

让我们看看为什么这些方法是不需要的。这两个调用可以从MyObject 或其基础之一调用虚函数。但是,MyObject::MyObject() 也可以安全地调用这些函数。在MyObject::MyObject() 返回后不会发生任何事情,这会使obj-&gt;Initialize() 安全。所以要么obj-&gt;Initialize() 是错误的,要么它的调用可以转移到MyObject::MyObject()。同样的逻辑反过来适用于obj-&gt;AboutToDelete()。派生最多的析构函数将首先运行,它仍然可以调用所有虚函数,包括AboutToDelete()

【讨论】:

除非在 MyObject 的子类中重新实现 Initialize(),并且我需要调用子类的实现,而不是 MyObject::Initialize()。从 MyObject 构造函数调用,它不会做我需要它做的事情。 (从 MyObject::~MyObject() 调用时,AboutToDelete() 有同样的问题)无论如何,“MyObject::MyObject() 返回后发生的事情”是子类构造函数的执行......那些需要发生在之前Initialize() 运行。 AboutToDelete() 的逻辑相反,它需要在任何子类析构函数运行之前运行。 这显然不是这里的情况,因为new MyObject 直接在调用之前。而您的反例只是更改名称。当所有不变量都已建立并且可以调用所有虚函数时,派生最多的构造函数最后运行。该 ctor 仍然可以调用 Initialize() 派生最多的构造函数不能安全地调用Initialize(),因为它不能确定派生最多的构造函数。很可能是另一个类对其进行了子类化,在这种情况下 Initialize() 将被调用得太早。 这就是为什么在这种情况下每个类都提供一个不调用 Initialize() 的受保护 ctor。 -1:“让我们说明为什么不需要这些方法”:我认为您对类设计的思考不够。有可能在析构函数和构造函数期间调用虚方法可以强制执行一些有用的行为。例如,我有一个基类 VideoPlayer 想在析构函数上调用 Stop。使用 C++ 我不能,因为可能仍然有线程调用虚拟方法。使用 C#,这是非常安全的,因为构造和销毁的工作方式完全不同,并且不会像 C++ 规范要求那样验证/无效对象的某些部分。【参考方案7】:

尚未看到答案,但基类只是在类层次结构中添加代码的一种方式。您还可以创建旨在添加到层次结构另一端的类:

template<typename Base> 
class Derived : public Base 
    // You'd need C++0x to solve the forwarding problem correctly.
    Derived() : Base() 
        Initialize();
    
    template<typename T>
    Derived(T const& t): Base(t) 
        Initialize();
    
    //etc
private:
    Initialize();
;

【讨论】:

【参考方案8】:

我遇到了同样的问题,经过一番研究,我相信没有任何标准解决方案。

我最喜欢的建议是 Aleksandrescu 等人提供的建议。书《C++ 编码标准》第 49 条。

引用它们(合理使用),您有多种选择:

    只需记录下您需要第二种方法,就像您所做的那样。 具有另一个内部状态(布尔值),用于标记是否已进行后期构建 使用虚拟类语义,即派生最多的类的构造函数决定使用哪个基类 使用工厂函数。

详情请参阅他的书。

【讨论】:

【参考方案9】:

您可以在类中使用静态函数模板。与私人 ctor/dtor。 在 vs2015 社区上运行

class A 
    protected:
    A() 
        virtual ~A() 
        virtual void onNew() = 0;
        virtual void onDelete() = 0;
    public:

        void destroy() 
            onDelete();
            delete this;
        

        template <class T> static T* create() 
            static_assert(std::is_base_of<A, T>::value, "T must be a descendant of A");
            T* t = new T();
            t->onNew();
            return t;
        
   ;

class B: public A 
     friend A;

     protected:
          B() 
          virtual ~B() 

          virtual void onNew() override 
          

          virtual void onDelete() override 
          
;

int main() 
    B* b;
    b = A::create<B>();
    b->destroy();

【讨论】:

以上是关于有没有自动化的方法来实现构造函数后和析构函数前的虚拟方法调用?的主要内容,如果未能解决你的问题,请参考以下文章

绝不在构造函数和析构函数中调用虚函数

绝不在构造函数和析构函数中调用虚函数

C++在构造函数中调用最终的虚函数

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

9. 构造函数和析构函数

条款09:不要在构造过程和析构过程中调用 virtual 方法