从构造函数调用虚函数

Calling a virtual function from the constructor

本文关键字:函数 函数调用      更新时间:2023-10-16

我正在阅读Effective c++ ,其中有"第9条:永远不要在构造或销毁期间调用虚函数"。我想知道如果我的代码是好的,即使它打破了这个规则:

using namespace std;
class A{
    public:
        A(bool doLog){
            if(doLog)
               log();
        }
        virtual void log(){
            cout << "logging An";
        }
};

class B: public A{
public:
    B(bool doLog) : A(false){
        if(doLog)
            log();
    }
    virtual void log(){
        cout << "logging Bn";
    }
};

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

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

在我看来,大多数答案都没有理解我在这里所做的,他们只是再次解释了为什么从构造函数调用虚函数有潜在的危险。

我想强调一下我的程序的输出是这样的:

logging A
logging B

所以当它被构造时,我得到A记录,当它被构造时,我得到B记录。这就是我想要的!但我问你是否发现任何错误(潜在的危险)与我的"hack"克服在构造函数中调用虚函数的问题。

这种方法有问题吗?

Bjarne Stroustrup的回答:

可以从构造函数调用虚函数吗?

是的,但是要小心。它可能不会如你所愿。在构造函数中,虚拟调用机制被禁用,因为重写来自派生的课程还没有开始。物体是从基础向上构造的,"派生前的基础"。考虑:

    #include<string>
    #include<iostream>
    using namespace std;
class B {
public:
    B(const string& ss) { cout << "B constructorn"; f(ss); }
    virtual void f(const string&) { cout << "B::fn";}
};
class D : public B {
public:
    D(const string & ss) :B(ss) { cout << "D constructorn";}
    void f(const string& ss) { cout << "D::fn"; 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。

有人建议该规则是一个实现工件。事实并非如此。实际上,实现从构造函数调用虚函数的不安全规则要明显容易得多,就像从其他函数调用一样。然而,这意味着不能编写虚函数来依赖基类建立的不变量。那将是一个可怕的混乱。

我想知道我的代码是否很好,即使它违反了这个规则:

这取决于你对"好"的定义。你的程序是格式良好的,它的行为是定义良好的,所以它不会调用未定义的行为和诸如此类的东西。

然而,当看到对虚函数的调用时,人们可能期望通过调用覆盖该函数的最派生类型提供的实现来解析该调用。

除了在构造过程中,相应的子对象还没有被构造,所以派生最多的子对象是当前正在构造的子对象。结果:调用被分派,就好像函数不是虚函数一样。

这是违反直觉的,你的程序不应该依赖于这个行为。因此,作为一名有文化的程序员,您应该习惯避免这样的模式,并遵循Scott Meyer的指导方针。

在定义良好的意义上,它是"好的"。在做你期望的事情的意义上,它可能不是"好"的。

你将从当前正在构造(或销毁)的类调用重写,而不是最终的重写;因为最终的派生类还没有被构造(或者已经被销毁),所以不能被访问。因此,如果您希望在这里调用最终重写,可能会遇到麻烦。

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

如果虚函数在该类中是纯虚函数,则不能从构造函数或析构函数调用虚函数;这是未定义行为