从构造函数调用虚函数

6

我正在阅读 Effective C++,其中有“第9条:不在构造和析构过程中调用虚函数”。我想知道即使违反这个规则,我的代码是否仍然可以正常工作:

using namespace std;

class A{
    public:
        A(bool doLog){
            if(doLog)
               log();
        }

        virtual void log(){
            cout << "logging A\n";
        }
};


class B: public A{
public:
    B(bool doLog) : A(false){
        if(doLog)
            log();
    }

    virtual void log(){
        cout << "logging B\n";
    }
};


int main() {
    A a(true);
    B b(true);
}

这种方法有问题吗?当我做更复杂的事情时,我会遇到麻烦吗?

我觉得大多数答案没有理解我所做的,他们只是再次解释为什么从构造函数调用虚函数可能存在危险。

我想强调一下,我的程序输出如下:

logging A
logging B

当构造函数被调用时,我希望能够记录A,当它被构建时,记录B。这正是我想要的!但我想知道您是否发现我的“hack”存在任何错误(可能危险),以克服在构造函数中调用虚拟函数的问题。


1
在这种情况下,你修补得很好,但如果其他人需要使用那段代码,错误是不可避免的。 - Marc Claesen
2
@JoachimPileborg 这不是真的:行为已经被定义。在构造期间,虚函数调用被禁用(例如使用当前正在构建的类型的实现)。 - Marc Claesen
3个回答

16

这种方法有什么问题吗?

Bjarne Stroustrup 的回答:

我能在构造函数中调用虚函数吗?

可以,但要小心。它可能不会按照你的预期执行。在构造函数中, 虚函数调用机制被禁用,因为来自派生类的重载尚未发生。对象是从基类向上构造的, “先基类后派生类”。请考虑:

    #include<string>
    #include<iostream>
    using namespace std;

class B {
public:
    B(const string& ss) { cout << "B constructor\n"; f(ss); }
    virtual void f(const string&) { cout << "B::f\n";}
};

class D : public B {
public:
    D(const string & ss) :B(ss) { cout << "D constructor\n";}
    void f(const string& ss) { cout << "D::f\n"; s = ss; }
private:
    string s;
};

int main()
{
    D d("Hello");
}

该程序编译并生成

B constructor
B::f
D constructor

注意不要调用D::f()。考虑如果规则不同,即从B::B()调用D::f()会发生什么:因为构造函数D::D()尚未运行,D::f()将尝试将其参数赋值给未初始化的字符串s。结果很可能是立即崩溃。

销毁按“派生类在基类之前”的顺序进行,因此虚函数的行为与构造函数中的行为相同:仅使用本地定义 - 不调用重写函数以避免触及对象的(现在已销毁的)派生类部分。

更多细节请参见D&E 13.2.4.2或TC++PL3 15.4.3。

有人认为这个规则是一种实现工件。事实并非如此。实际上,以与其他函数完全相同的方式调用构造函数中的虚函数的不安全规则会更容易实现。但是,这将意味着不能编写依赖于基类建立的不变量的虚函数。那将是一团糟。


11

我在想即使违反这个规则,我的代码是否仍然可以正常工作:

这取决于你所谓的“正常工作”的含义。你的程序是格式良好的,并且其行为是明确定义的,因此它不会调用未定义的行为或类似的内容。

但是,当看到调用虚函数时,人们可能会期望调用由覆盖该函数的最派生类型提供的实现。

然而,在构造期间,相应的子对象尚未被构造,因此最派生的子对象是当前正在构造的对象。结果:调用被分派为如果该函数不是虚拟的那样。

这是反直觉的,你的程序不应依赖这种行为。因此,作为一名文学程序员,你应该习惯避免这种模式并遵循 Scott Meyer 的指南。


讲解得很清楚,但我必须说我觉得这非常直观。我用C++编程多年,后来发现有些语言调用最派生类的版本时,我感到震惊和愤怒。 - Steve

4

从定义上讲,它是“fine”的。但可能不会在您期望的情况下表现得“fine”。

您将从正在构造(或销毁)的类中调用覆盖,而不是最终覆盖;因为最终派生类尚未构造(或已经销毁),因此无法访问。因此,如果您希望在此处调用最终覆盖,则可能会遇到麻烦。

由于这种行为可能会引起混淆,最好避免这样做。在这种情况下,我建议通过聚合向类添加行为而不是子类化。类成员在构造函数体之前构造,并持续到析构函数之后,因此在这两个地方都可用。

有一件事情是绝不能做的,那就是在类中的纯虚拟函数中从构造函数或析构函数调用虚拟函数;这是未定义的行为。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接