12.使用override来修饰需要覆写的虚函数

前言

 
OOP的最重要的特性即为多态,其具体表现为派生类中的虚函数实现override了基类的实现。本节将重点讨论使用override确保你真正完成了override操作。


override约束

 
尽管override与overload长得很像,但实际上根本就不是一回事。下面将通过一个实例展示如何通过一个基类接口调用派生类函数:

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
virtual void doWork(); // base class virtual function

};
class Derived: public Base {
public:
virtual void doWork(); // overrides Base::doWork virtual" is optional here

};
std::unique_ptr<Base> upb = std::make_unique<Derived>();
upb->doWork(); // call doWork through base class ptr derived class function is invoked

发生需要满足以下要求:

  1. 基类函数必须为虚函数。
  2. 基类函数与派生类函数名称必须相同(析构函数除外)。
  3. 基类函数与派生类函数形参类型必须相同。
  4. 基类函数与派生类函数必须具备同样的constness。
  5. 基类函数与派生类函数返回值类型与异常规格必须匹配(至少可以转换?)

在这些基础之上,C++11又增加了一个新的要求:

  1. 基类函数与派生类函数的reference qualifier(引用限定符)必须相同。
    成员函数reference qualifier是C++11不太为人所熟知的特性之一,其功能为将成员函数的使用限定为左值或右值,它们的存在与virtual不具备关联性,以下为实例展示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Widget {
    public:

    void doWork() &; // this version of doWork applies only when *this is an lvalue
    void doWork() &&; // this version of doWork applies only when *this is an rvalue
    };

    Widget makeWidget(); // factory function (returns rvalue)
    Widget w; // normal object (an lvalue)

    w.doWork(); // calls Widget::doWork for lvalues (i.e., Widget::doWork &)
    makeWidget().doWork(); // calls Widget::doWork for rvalues (i.e., Widget::doWork &&)
    有关reference qualifier的介绍以后再说,这里需要强调的是,如果派生类需要override基类函数,那么它们需要具备同样的refernce qualifier,否则基类版本将依旧出现在派生类之中并且不会被override。

override声明

 
上述约束表明即使一个很小的失误也可能会导致override无法成功,并且编译器将不会对没有发生override的情况进行通知。例如下个实例中并没有发生override,你能够定位出是哪些地方出了问题吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
void mf4() const;
};
class Derived: public Base {
public:
virtual void mf1();
virtual void mf2(unsigned int x);
virtual void mf3() &&;
void mf4() const;
};

答案分别为:

  1. mf1在基类中为const
  2. mf2在基类中形参为int
  3. mf3在基类中被限定为左值
  4. mf4在基类中没有被声明为virtual

鉴于派生类中声明的virtual函数很容易无法覆写基类中对应的函数,并且编译器并不会对未发生override的行为发出警告,因此C++11提供了一种显式的声明方式表示当前函数需要override基类中对应的函数,其使用方式如下:

1
2
3
4
5
6
7
class Derived: public Base {
public:
virtual void mf1() override;
virtual void mf2(unsigned int x) override;
virtual void mf3() && override;
virtual void mf4() const override;
};

上述代码将无法通过编译,因为编译器会认真地检查其是否如实override了基类版本,这正是我们想要的。真正能够通过编译的代码有形式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
virtual void mf4() const;
};
class Derived: public Base {
public:
virtual void mf1() const override;
virtual void mf2(int x) override;
virtual void mf3() & override;
void mf4() const override; // adding "virtual" is OK,
};


override优点

 
使用override不仅仅能够让编译器告诉你某函数是否发生了override,还能够在你试图更改基类虚函数签名时观察到对派生类的影响。只需要在更改签名后查看有多少派生类函数编译失败,即可权衡更改签名是否值得。如果没有override声明,那你则不得不需要一个全面的单元测试以判断所有分支。

C++11引入的两个新关键词final与override均为contextual keywords,即只有在特定环境下才有其存在意义。以override来说,它仅出现于成员函数声明式末尾。这也就是说历史遗留代码中的名称override依然合法,不会触发标识符不合法的行为:

1
2
3
4
5
class Warning { // potential legacy class from C++98
public:

void override(); // legal in both C++98 and C++11 with the same meaning
};


reference qualifier

 
如果你希望某个函数只接受左值或右值参数,那你完全可以这样声明:

1
2
void doSomething(Widget& w); // accepts only lvalue Widgets
void doSomething(Widget&& w); // accepts only rvalue Widgets

reference qualifier的作用与之类似,其形参可以看作为*this,它通过判断*this的左右属性来进行决断,从某种意义而言,非常类似于const成员函数。

举例而言,我们的Widget类有一个std::vector数据成员,并且我们给用户提供了一个接口以确保用户可以直接访问它:

1
2
3
4
5
6
7
8
class Widget {
public:
using DataType = std::vector<double>; // see in Item9
DataType& data() { return values; }

private:
DataType values;
};

显然这个东西根本不能体现出一丝一毫的封装性,但我们先不管他,看看用户可能进行的操作:
1
2
3
Widget w;

auto vals1 = w.data(); // copy w.values into vals1

由于w.data()返回一个左值引用,因此vals的类型被推衍为vector,利用values初始化之。
假设我们当前有一个工厂函数:
1
Widget makeWidget();

并且我们希望利用工厂函数生成的无名对象(临时对象)来初始化一个vector:
1
auto vals2 = makeWidget().data(); // copy values inside the Widget into vals2

和上一次不同的是,本次设计不应当执行copy操作,因为从临时对象中获取数据使用move更好,但由于data返回一个左值引用,所以拷贝依然会发生。显然这里存在可以优化之处,但试图让编译器给你优化是不切实际的。因此我们需要使用reference qualifier来限定当widget为右值时data()返回值也应当是一个右值:
1
2
3
4
5
6
7
8
9
10
class Widget {
public:
using DataType = std::vector<double>;

DataType& data() & { return values; } // for lvalue Widgets,return lvalue
DataType data() && { return std::move(values); } // for rvalue Widgets,return rvalue

private:
DataType values;
};

一切大功告成。


总结

 

  1. 将需要覆写的函数声明为override。
  2. reference qualifier可以针对对象的左右值属性实现重载。