从构造函数中调用虚函数

时间:2013-05-23 16:43:05

标签: c++ constructor virtual-functions

我正在阅读 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。这就是我想要的!但我问你是否发现任何错误(有潜在危险)我的“黑客”来克服在构造函数中调用虚函数的问题。

3 个答案:

答案 0 :(得分:15)

  

此方法有问题吗?

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。结果很可能是立即崩溃。   销毁是在“基类之前的派生类”完成的,因此虚函数的行为与构造函数一样:只使用本地定义 - 并且不会调用重写函数以避免触及对象的(现在销毁的)派生类部分。 / p>      

有关详细信息,请参阅D&amp; E 13.2.4.2或TC ++ PL3 15.4.3。

     

有人建议此规则是实现工件。不是这样。实际上,实现从构造函数调用虚函数的不安全规则与从其他函数完全相同,会明显更容易。但是,这意味着不能编写任何虚函数来依赖基类建立的不变量。那将是一个糟糕的混乱。

答案 1 :(得分:11)

  

我想知道即使违反这条规则,我的代码是否合适:

这取决于你所说的“好”。您的程序格式正确,其行为定义明确,因此不会调用未定义的行为和类似的行为。

但是,当看到对虚函数的调用时,可以预期通过调用覆盖该函数的最派生类型提供的实现来解析调用。

除了在构造期间,尚未构造相应的子对象,因此最派生的子对象是当前正在构造的子对象。结果:调度调用,就像函数不是虚拟的一样。

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

答案 2 :(得分:4)

在定义明确的意义上,它是“好的”。在做你期望的意义上,这可能不是“好”。

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

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

你不能做的一件事就是从构造函数或析构函数中调用一个虚函数,如果它在该类中是纯虚函数的话。这是未定义的行为。