代码之家  ›  专栏  ›  技术社区  ›  Yona

在基类构造函数中调用虚方法

  •  32
  • Yona  · 技术社区  · 17 年前

    我知道从基类构造函数调用虚方法可能是危险的,因为子类可能不处于有效状态。(至少在C#中)

    我的问题是,如果虚拟方法是初始化对象状态的方法,该怎么办?这是一种良好的做法,还是应该分两步进行,首先创建对象,然后加载状态?

    第一个选项:(使用构造函数初始化状态)

    public class BaseObject {
        public BaseObject(XElement definition) {
            this.LoadState(definition);
        }
    
        protected abstract LoadState(XElement definition);
    }
    

    第二种选择:(使用两步流程)

    public class BaseObject {
        public void LoadState(XElement definition) {
            this.LoadStateCore(definition);
        }
    
        protected abstract LoadStateCore(XElement definition);
    }
    

    // The base class will call the virtual method to load the state.
    ChildObject o = new ChildObject(definition)
    

    在第二种方法中,使用者必须创建对象,然后加载状态:

    ChildObject o = new ChildObject();
    o.LoadState(definition);
    
    9 回复  |  直到 12 年前
        1
  •  39
  •   Jon Skeet    17 年前

    (这个答案适用于C++和java。我相信C++在这方面的工作方式不同。)

    在构造函数中调用虚方法确实是危险的,但有时它会以最干净的代码结束。

    极大地 在构造函数中使用虚拟方法,并将其记录下来 强烈地只要所有参与的人都知道它在做什么,就不应该导致 很多问题。不过,我会尽量限制可见性,正如您在第一个示例中所做的那样。

    public class Child : Parent
    {
        private int foo = 10;
    
        protected override void ShowFoo()
        {
            Console.WriteLine(foo);
        }
    }
    

    在哪里 Parent ShowFoo ,在C#中,它将显示10。Java中的等效程序将显示0。

        2
  •  10
  •   Greg Rogers    17 年前

    在C++中,调用基类构造函数中的虚拟方法将简单地调用该方法,就好像派生类还不存在(因为它不存在)。这意味着调用在编译时被解析为它应该在基类(或它派生的类)中调用的任何方法。

    通过GCC测试,它允许您从构造函数调用纯虚拟函数,但它会发出警告,并导致链接时间错误。该行为似乎未被标准定义:

    成员函数可以从抽象类的构造函数(或析构函数)调用;进行虚拟调用的效果( )对于从此类构造函数(或析构函数)创建(或销毁)的对象,直接或间接向纯虚函数的调用是未定义的。”

        3
  •  4
  •   Rob Walker    17 年前

    使用C++,虚拟方法通过VTABLE路由到正在构建的类。因此,在您的示例中,它将生成一个纯虚拟方法异常,因为在构造BaseObject时,根本没有LoadStateCore方法可调用。

    如果函数不是抽象的,而只是什么都不做,那么您经常会让程序员挠头,试图记住为什么函数实际上没有被调用。

    因为这个原因,你不能用C++的方式来做…

        4
  •  4
  •   Brian    17 年前

    对于C++,在派生构造函数之前调用基本构造函数,这意味着虚表(它包含派生类的重写虚函数的地址)还不存在。因此,这被认为是一件非常危险的事情(特别是如果函数在基类中是纯虚拟的……这将导致纯虚拟异常)。

    1. 执行构造+初始化的两步流程
    2. 将虚拟函数移动到您可以更紧密控制的内部类(可以使用上述方法,请参见示例了解详细信息)

    (1)的一个例子是:

    class base
    {
    public:
        base()
        {
          // only initialize base's members
        }
    
        virtual ~base()
        {
          // only release base's members
        }
    
        virtual bool initialize(/* whatever goes here */) = 0;
    };
    
    class derived : public base
    {
    public:
        derived ()
        {
          // only initialize derived 's members
        }
    
        virtual ~derived ()
        {
          // only release derived 's members
        }
    
        virtual bool initialize(/* whatever goes here */)
        {
          // do your further initialization here
          // return success/failure
        }
    };
    

    class accessible
    {
    private:
        class accessible_impl
        {
        protected:
            accessible_impl()
            {
              // only initialize accessible_impl's members
            }
    
        public:
            static accessible_impl* create_impl(/* params for this factory func */);
    
            virtual ~accessible_impl()
            {
              // only release accessible_impl's members
            }
    
            virtual bool initialize(/* whatever goes here */) = 0;
        };
    
        accessible_impl* m_impl;
    
    public:
        accessible()
        {
            m_impl = accessible_impl::create_impl(/* params to determine the exact type needed */);
    
            if (m_impl)
            {
                m_impl->initialize(/* ... */);  // add any initialization checking you need
            }
        }
    
        virtual ~accessible()
        {
            if (m_impl)
            {
                delete m_impl;
            }
        }
    
        /* Other functionality of accessible, which may or may not use the impl class */
    };
    

    方法(2)使用工厂模式为 accessible 类(它将提供与您的 base 可到达的 能够利用虚拟成员的 accessible_impl 安全地

        5
  •  3
  •   David Thornley    17 年前

    对于C++,第12.7节第3段的标准涵盖了这种情况。

    总而言之,这是合法的。它将根据正在运行的构造函数的类型解析为正确的函数。因此,将您的示例应用到C++语法中,您将调用 BaseObject::LoadState() . 你不能去 ChildObject::LoadState() ,并试图通过指定类和函数来执行此操作,从而导致未定义的行为。

    第10.4节第6段介绍了抽象类的构造函数。简言之,它们可以调用成员函数,但在构造函数中调用纯虚函数是未定义的行为。不要那样做。

        6
  •  3
  •   Greg Beech    17 年前

    如果您有一个类,如您的帖子所示,它需要 XElement 在构造函数中,那么 希莱门特 希莱门特

    public class BaseClass
    {
        public BaseClass(XElement defintion)
        {
            // base class loads state here
        }
    }
    
    public class DerivedClass : BaseClass
    {
        public DerivedClass (XElement defintion)
            : base(definition)
        {
            // derived class loads state here
        }
    }
    

    这样,您的代码就非常简单,并且您没有任何虚拟方法调用问题。

        7
  •  3
  •   Özgür    17 年前

    Never Call Virtual Functions during Construction or Destruction

    注:请注意本文中的例外情况:

    在运行前变得明显, 交易中的纯虚拟。除非 已经定义( 不太可能,但是 )程序无法链接:链接器将无法找到Transaction::logTransaction的必要实现。

        8
  •  1
  •   Mark Brackett    17 年前

    通常,您可以通过使用更贪婪的基本构造函数来解决这些问题。在您的示例中,将XElement传递给LoadState。如果允许在基本构造函数中直接设置状态,则子类可以在调用构造函数之前解析XElement。

    public abstract class BaseObject {
       public BaseObject(int state1, string state2, /* blah, blah */) {
          this.State1 = state1;
          this.State2 = state2;
          /* blah, blah */
       }
    }
    
    public class ChildObject : BaseObject {
       public ChildObject(XElement definition) : 
          base(int.Parse(definition["state1"]), definition["state2"], /* blah, blah */) {
       }
    }
    

    如果子类需要做大量的工作,它可以转移到静态方法。

        9
  •  1
  •   Johannes Schaub - litb    17 年前

    在C++中,从基类中调用虚拟函数是完全安全的,只要它们是 非纯 initialize )。如果在调用它的类中将其声明为纯虚拟,则该行为未定义。

    调用的版本是从构造函数中调用它的类的版本,而不是某个派生类中的某个重写器。这与虚拟函数表没有多大关系,但更重要的是,该函数的重写可能属于尚未初始化的类。所以这是禁止的。

    在C#和Java中,这不是问题,因为没有在进入构造函数主体之前完成的默认初始化。在C#中,唯一在主体之外完成的事情是调用基类或兄弟构造函数,我相信。但是,在C++中,当在进入派生类的构造函数体之前处理构造函数初始化列表时,在函数的重写器中对派生类成员进行初始化时,将不进行初始化。

    编辑 :由于有评论,我认为需要澄清一下。下面是一个(人为的)示例,假设允许调用virtuals,并且调用将导致激活最终重写器:

    struct base {
        base() { init(); }
        virtual void init() = 0;
    };
    
    struct derived : base {
        derived() {
            // we would expect str to be "called it", but actually the
            // default constructor of it initialized it to an empty string
        }
        virtual void init() {
            // note, str not yet constructed, but we can't know this, because
            // we could have called from derived's constructors body too
            str = "called it";
        }
    private:
        string str;
    };
    

    这个问题确实可以通过改变C++标准并允许它来调整——构造函数的定义、对象生存期和不发生的事情来解决。必须制定规则来定义什么 str = ...; 表示尚未构造的对象。请注意,它的效果如何取决于打电话的人 init . 我们得到的特性不能证明我们当时必须解决的问题是正确的。因此C++在构建对象时,简单地禁止动态调度。