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”有什么问题(可能很危险),以克服在构造函数中调用虚函数的问题。

4

3 回答 3

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

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

有人建议此规则是一个实现工件。不是这样。事实上,实现从构造函数调用虚函数的不安全规则与从其他函数调用虚函数一样容易得多。但是,这意味着不能编写任何虚函数来依赖基类建立的不变量。那将是一个可怕的混乱。

于 2013-05-23T16:47:33.957 回答
11

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

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

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

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

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

于 2013-05-23T16:48:28.897 回答
4

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

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

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

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

于 2013-05-23T17:00:56.803 回答