• [译]GotW #5:Overriding Virtual Functions


         虚函数是一个很基本的特性,但是它们偶尔会隐藏在很微妙的地方,然后等着你。如果你能回答下面的问题,那么你已经完全了解了它,你不太能浪费太多时间去调试类似下面的问题。

    Problem

    JG Question

    1. override和final这两个关键字都有什么作用?为什么他们有用?

    Guru Qusetion

    2. 在你浏览公司的代码的时候,你看到了一个未知程序员写的下面的代码片段。这个程序员好像看起来是在练习一些C++特性,想看下它们是怎么工作的。

        (a)怎么做能改进下面代码的正确性或风格?

        (b)这个程序员可能期待程序打印什么,但实际上是怎么一个情况?

    class base {
    public:
        virtual void f( int );
        virtual void f( double );
        virtual void g( int i = 10 );
    };
    
    void base::f( int ) {
        cout << "base::f(int)" << endl;
    }
    
    void base::f( double ) {
        cout << "base::f(double)" << endl;
    }
    
    void base::g( int i ) {
        cout << i << endl;
    }
    
    class derived: public base {
    public:
        void f( complex<double> );
        void g( int i = 20 );
    };
    
    void derived::f( complex<double> ) {
        cout << "derived::f(complex)" << endl;
    }
    
    void derived::g( int i ) {
        cout << "derived::g() " << i << endl;
    }
    
    int main() {
        base    b;
        derived d;
        base*   pb = new derived;
    
        b.f(1.0);
        d.f(1.0);
        pb->f(1.0);
    
        b.g();
        d.g();
        pb->g();
    
        delete pb;
    }

    Stop and thinking…..

    Solution

    1. override和final这两个关键字都有什么作用?为什么他们有用?

         这些关键字的功能是对虚函数的重写有了明确的控制。在声明中使用override的意图是重写基类的虚函数。而final则是让基类的虚函数在子类中变得不再具有可重写性,或一个类不再允许有子类。

         它们有用是因为它们让程序员在编译期对函数的声明有了更明确意图。如果你在声明中写了override而在基类中找不到匹配的虚函数,或你声明为final而在派生类中试图隐式或显式地重写函数,那么在编译期就会有错误。

         两者之中,到目前为止,更常见的使用的是override,使用final很少见。

    2. (a)怎么做能改进下面代码的正确性或风格?
         首先,看一看一些风格问题,这里有个具体的错误:
    1.代码中显示地使用new、delete和*

         避免使用原始指针和显示使用new和delete,除了在你试图写一写底层数据结构的内部实现时。

    {
        base*   pb = new derived;
    
        ...
    
        delete pb;
    }

         使用make_unique和unique_ptr<base>来替代new和base*

    {
        auto pb = unique_ptr<base>{ make_unique<derived>() };
    
        ...
    
    } // automatic delete here

            Guideline: 不要显示地使用new和delete和拥有*指针。除了封装在一些底层数据结构的内部实现时。

         但是,delete带来了另外一个不相干的问题,如何分配和管理对象的生命期,也就是:


    2.基类的析构函数应该是virtual或protected

    class base {
    public:
        virtual void f( int );
        virtual void f( double );
        virtual void g( int i = 10 );
    };

         这看起来是无伤大雅的事,但是在base类中既没有使得析构函数为virtual也没有是protected。事实上,通过指向没有virtual析构函数的基类的指针来删除对象是一件邪恶的事。因为派生类的成员不会被销毁,并且delete操作符会以不正确的对象大小被调用。

           Guideline: 使基类的析构函数是public且virtual,或protected且non-virtual

    下面的其中一项会适用于一个多态类型:
          · 要不允许通过指向基类的指针来析构,此时析构函数必须是public并且最好是virtual
          · 或者不通过,此时析构函数必须是protected(private是不被允许的,因为派生类析构函数必须能够调用基类析构函数)且是non-virtual(当派生类析构函数调用基类析构函数,不论声明是否为virtual,它确实是non-virtual的)

    插曲
    对于接下来的问题,有必要区分一下下面三个术语:
        · 重载(overload)函数f的意思是在相同作用域中提供另外一个有着相同名字但不同参数类型的函数。当调用f时,编译器基于具体的类型尝试去匹配最适合的一个
        · 重写(override)虚函数f的意思是在派生类中,提供另外一个有着相同名字和参数类型的函数。
        · 隐藏(hide)函数f在一个存在的封闭作用域中(基类、外部类或名字空间)的意思是在内部作用域中(派生类、嵌套类或名字空间)提供一个相同名字的函数,此时将会隐藏外部作用域中的相同名字的函数。

    3.derived::f既不是重写也不是重载

    void derived::f( complex<double> )

         派生类derived没有重载base::f函数,而是隐藏了它。这个区别很重要,因为这意味着base::f(int)和base::f(double)在派生类derived的作用域中是不可见的。

         如果写derived的程序员确实是想要隐藏基类中同名的函数f,那么这是正确的。但是一般来说,隐藏可能是一时的疏忽,正确的做法是将它的名字带入派生类的作用域中,比如在derived类中这样写:using base::f。

             Guideline: 当提供一个非重写的同名函数作为继承而来的函数时,如果你不想隐藏它们,确保在作用域中对继承而来的函数使用using-声明。

    4.derived::g重写了base::g,但是声明中却没有“override”

    void g( int i = 20 )  /* override */

         这个函数重写了基类的函数,因此它应该显示地写上override。这样就记录了它的意图,且如果你试图去重写一个不是虚的函数或者将函数签名误写了的话,此时编译器会提醒你。

             Guideline: 当有意去重写虚函数时,应该总是写上override。

    5.derived::g重写了base::g但改变了默认参数。

    void g( int i = 20 )

         改变默认参数是很明确的用户不友好行为。除非你真的是想去迷惑使用它的人,否则的话不要改变你重写函数的默认参数。这个在C++是合法的,且结果也是定义良好的,但是不要那么做。下面我们会看到它如何让人感到困惑。

        Guideline: 绝不要改变重写函数的默认参数

    或者可以进一步:


        Guideline: 一般情况下载虚函数中避免有默认参数

    最后,公有的虚函数是好的,当这个类是一个纯抽象基类(abstract base class --ABC),只指定虚接口而不实现它们,就像C#或java中的interface那样。

        Guideline: 倾向于一个类只有公有的虚函数,或非公有的虚函数()除了特别的析构函数


                       一个纯抽象基类应该只有公有的虚函数。

         但是当一个类既有虚函数又有它们的实现,考虑使用Non-Virtual Interface(NVI),这样区分公有接口和虚接口。对于任何其他基类,倾向于使公有成员函数为non-virtual且虚成员函数为非公有。前者应该有默认参数并且是依据后者来实现。这很清晰地将公有接口从派生接口中分离了出来,让它们遵循它们自然的格式来适应不同的用户,且避免了一个函数有两个责任来做两份事。其他的好处是,使用NVI经常可以在一些重要方面阐明你的类的设计。比如对于用户重要的默认参数来说,它应该属于公有接口而不是虚接口。

    2. (b)这个程序员可能期待程序打印什么,但实际上是怎么一个情况?

         现在我们已经把这些问题解决了,来看看主函数中是否确实是做了这个程序员想要做的事:

    int main() {
        base    b;
        derived d;
        base*   pb = new derived;
    
        b.f(1.0);

         没问题,首先调用base::f(double),和想象中的那样。

    d.f(1.0);

         这会调用derived::f(complex<double>),为什么?,记住这里的derived类没有使用using base::f来将基类的f函数带入这个作用域,因此很明确,base::f(int)和base::f(double)不会被调用。它们没有出现在和derived::f(complex<double>)一样的作用域中。

         这个程序员可能是想要这调用base::f(double),但在这种情况下将不会,甚至会是编译错误,因为幸运(?)的是complex<double>提供了一个double的隐式转换,因此编译器将这个调用解释成derived::f( complex<double>(1.0) ).

    pb->f(1.0);

         有意思的是,尽管base* pb是指向derived对象,但这会调用base::f(double),因为函数重载解析是在静态类型上(base)完成的,而不是动态类型(derived)。base指针,base接口。基于同样原因,调用pb->f(complex<double>(1.0));将不会被编译,因为此时在基类中找不到匹配的函数。

    b.g();

    打印出10,因为这只是简单地调用base::g(int),默认的参数为10,毫不费力。

    d.g();

    打印出derived::g() 20,因为这只是调用derived::g(int),默认参数是20,同样毫不费力。

    pb->g();

    打印出derived::g() 10.

    你可能会奇怪,这究竟发生了什么?这个结果可能会让你的脑子短路一下下子,然后你意识到这就是编译器要做的事!要记住的事是,重载,默认的参数是取自于对象的静态类型(base),因此这里是10。然而,这个函数是虚函数,所以具体调用的函数是基于对象的动态类型(derived)。再一次,这可以通过避免在虚函数中使用默认参数来避免。或者使用NVI来完全避免公有虚函数。

    delete pb;

    最后,值得注意的是,这应该是不必要的,因为你应该使用unique_ptr,它会为你做最后的清理工作,同时base应该有个virtual析构函数,这样通过任意指向base的指针都能正确地析构。

    原文地址:http://herbsutter.com/2013/05/22/gotw-5-solution-overriding-virtual-functions/

  • 相关阅读:
    《需求工程——软件建模与分析》阅读笔记03
    第十一周周五
    统计字符串里每个词语的数目
    第十一周周四计划
    for循环创建的a标签,当点击时如何确定点击的是哪一个标签?
    第十周计划周二&周三计划
    第十周周二汇报
    第九周周五小思路
    第九周周五计划&&周四总结
    第九周周四计划&&周三总结
  • 原文地址:https://www.cnblogs.com/navono007/p/3404091.html
Copyright © 2020-2023  润新知