Effective C++——条款09:绝不在构造和析构的过程中调用virtual函数

Effective C++——条款09:绝不在构造和析构的过程中调用virtual函数,第1张

Never call virtual functions during construction or destruction

本条款开始之前我要先阐述重点:你不应该在构造函数和析构函数期间调用虚函数,因为这样的调用不会带来你预想打结果,就算有你也会不高兴。


 

假设我们有一个类,我们需要对其基本类型的初始化封装一个函数,方便代码复用,但是他的父类是一个纯虚基类,只定义了这样的接口,看似很好,我们需要自己重写这样的函数。


于是就这样。


#include 

using namespace std;

class AbsData
{
protected:
    int a;
    int b;
    int c;
    virtual void set(int _a=0,int _b=0, int _c=0)
    {
        a = _a;
        b = _b;
        c = _c;
    }
public:
    AbsData(int a, int b, int c) {
        set(a,b,c);
    }
};

class Data :public AbsData
{
private:
    string name;
public:
    Data(int a, int b, int c, const string &name) : AbsData(a, b, c), name(name) { }
    void set(int _a = 0, int _b = 0, int _c = 0) override
    {
        a = 2 * _a;
        b = 2 * _b;
        c = 2 * _c;
    }

};

int main()
{
    Data data(1,2,3,"box");
    return 0;
}

读者可能会想到,我们这写很合理,在基类的构造中写一个用于构造的虚函数,子类对象根据多态会调用子类的set方法。


这样很好,不是吗?

错误,大错特错!!!在编写构造和析构函数的时候,请永远记住本条款的规则。


 我们这样做除了得到一个链接期间的错误还会得到编译器的一堆警告。


包括但不限于

Clang-Tidy: Default arguments on virtual or override methods are prohibited

Clang-Tidy:禁止虚拟或覆盖方法的默认参数

Call to pure virtual member function 'set' has undefined behavior; overrides of 'set' in subclasses are not available in the constructor of 'AbsData'

对纯虚成员函数“set”的调用具有未定义的行为; 子类中的“set”覆盖在“AbsData”的构造函数中不可用

Clang-Tidy: Constructor does not initialize these fields: a, b, c

Clang-Tidy:构造函数不初始化这些字段:a、b、c

Do not invoke virtual member functions from constructor

不要从构造函数调用虚成员函数

仅仅从编译器的警告中就可以看见这是一个多么糟糕的设计。


笔者注:

笔者这里没有使用书中作者的例子,这也许是自作聪明,或者是画蛇添足,但是笔者感觉应当还是能够更清晰的说明这个条款的问题。


 

一个众所周知的机制:继承体系中的基类总是优先调用构造函数。


我们看一下其中一段代码。


 

    AbsData() {
        set();
    }

 这个代码块总是会被先调用——问题就出现在这里,就这个例子来说,在构造对象期间,构造函数调用的虚函数总是基类自己的。


而不是子类的版本。


是的,在基类构造期间的虚函数绝对不会下降到继承类阶层。


取而代之的是,对象的作为就像隶属于base一样(作者想表达的意思应该是这个要构造的对象就像一个基类对象一样)。


非正式的说法或许比较传神,在基类构造期间,虚函数不是虚函数。


这应当很好理解,我们也能为其说明合适的理由。


在基类的构造期间,怎么知道子类中有什么东西呢?既然不知道那又怎样去调用子类中的重写方法,就算C++真的能够让你调用(其实不能),那又能做些什么呢——因为属于子类中的数据成员还没有 ——因为子类没有开始被构造。


 

我们设想一下上面的假设成立(C++允许在构造基类中的时候将虚函数下降到子类层)会发生什么可怕的结果。


这将是一张通往不明确型为和彻夜调试大会串的直达车票。


“要求使用对象内未初始哈的成分”是危险的代名词,所以C++不会让你走这条路。


 其实还有比上述理由更根本的原因

在子类对象的基类部分构造期间,对象的类型是基类而不是子类,所以虚函数才会如此被解析。


不仅虚函数这样,就连使用RTTI也会把对象视为基类。


对于析构对象时也是一样,当自底向上析构的时候,析构到了哪个阶层,那么被析构的对象也就相应的类型。


笔者对这一部分进行了正是,我去掉所有的其他文字打印,只留下typeid的结果,将基类中的set改为非纯虚函数。


 于是得到了下面的结果

class AbsData
{
private:
    virtual void set(int a=0,int b=0, int c=0)
    {
        cout << typeid(this).name() << endl;
    }
public:
    AbsData() {
        set();
    }
};

class Data :public AbsData
{
private:
    void set(int _a=0, int _b=0, int _c=0) override {
        cout << typeid(this).name() << endl;
    }

public:
    Data(const string & _name, int _a, int _b, int _c) : name(_name)
    {
        set(_a, _b, _c);
    }
};

 结合上述来看,在析构函数中调用虚函数也是同样的违反规则。


笔者注:

在析构函数中调用自己本阶层的析构函数并不会出现什么错误,但是我们有什么样的理由去这样设计呢?请不要触碰危险的边缘,这是不明确行为和烂代码的开始。


 书中这个条款后面讲述的问题,其实就是笔者最开提出的问题。


所以笔者在这里不再写额外的代码了。


书接上文,如果在基类中的虚函数是纯的,这样倒不会有什么么问题,只是得到一个连接期间的错误——这相比不明确的行为可好太多。


但是如果不是纯的呢?链接依旧会正常进行,因为基类中有一份实现的代码,他会正常调用,程序依旧会正常进行,留下你百思不解为什么建立一个派生类对象时会调用错误版本的函数。


解决这个问题的办法之一就是将这个函数声明为非虚函数,然后要求继承类的构造函数必须传递信息给其基类的构造函数。


然后再安全的调用非虚函数。


也许最开始的代码可能优点不适当的说明这个问题,我们进行一些修改。



class AbsData
{
private:
    int a;
    int b;
    int c;
    void set(int _a=0,int _b=0, int _c=0)
    {
        a = _a;
        b = _b;
        c = _c;
    }
public:
    AbsData(int a, int b, int c) {
        set(a,b,c);
    }
};

class Data :public AbsData
{
private:
    string name;
public:
    Data(int a, int b, int c, const string &name) : AbsData(a, b, c), name(name) { }

};

这也是比较常规的手段。


 换句话说由于你无法使用虚函数从基类向下调用,在构造期间,你可以藉由 “令继承类将必要的构造信息向上传递至基类中的构造函数” 替换值加一弥补。


笔者这里没有说明使用一个静态函数赋值的创建一个对象用于传递给基类的构造函数,如果读者有兴趣,可以自行查阅(P51-P52)。


 

笔者注:

在C++11以上可以使用继承构造函数来进行代码的复用。


 

#include 

using namespace std;

class AbsData
{
public:
    int a;
    int b;
    int c;
    void set(int _a=0,int _b=0, int _c=0)
    {
        a = _a;
        b = _b;
        c = _c;
    }
public:
    AbsData(int a, int b, int c) {
        set(a,b,c);
    }
};

class Data :public AbsData
{
public:
    string name;
public:
    using AbsData::AbsData;
    Data(int a,int b, int c,const string & _name) : Data(a,b,c)
    {
        name = _name;
    }
};

int main()
{
    Data data(1,2,3,"box");
    cout << data.a << data.b << data.c << data.name;
    return 0;
}

 笔者的总结:就笔者写的这一些例子来说,在实际情况中一般都不会这样做,调用基类的构造函数时比较好的选择,这里只是为了说明问题。


请记住

在构造函数和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/634689.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-16
下一篇 2022-04-16

发表评论

登录后才能评论

评论列表(0条)

保存