前言
OOP的最重要的特性即为多态,其具体表现为派生类中的虚函数实现override了基类的实现。本节将重点讨论使用override确保你真正完成了override操作。
override约束
尽管override与overload长得很像,但实际上根本就不是一回事。下面将通过一个实例展示如何通过一个基类接口调用派生类函数:1
2
3
4
5
6
7
8
9
10
11
12class 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
发生需要满足以下要求:
- 基类函数必须为虚函数。
- 基类函数与派生类函数名称必须相同(析构函数除外)。
- 基类函数与派生类函数形参类型必须相同。
- 基类函数与派生类函数必须具备同样的constness。
- 基类函数与派生类函数返回值类型与异常规格必须匹配(至少可以转换?)
在这些基础之上,C++11又增加了一个新的要求:
- 基类函数与派生类函数的reference qualifier(引用限定符)必须相同。
成员函数reference qualifier是C++11不太为人所熟知的特性之一,其功能为将成员函数的使用限定为左值或右值,它们的存在与virtual不具备关联性,以下为实例展示:有关reference qualifier的介绍以后再说,这里需要强调的是,如果派生类需要override基类函数,那么它们需要具备同样的refernce qualifier,否则基类版本将依旧出现在派生类之中并且不会被override。1
2
3
4
5
6
7
8
9
10
11
12class 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 &&)
override声明
上述约束表明即使一个很小的失误也可能会导致override无法成功,并且编译器将不会对没有发生override的情况进行通知。例如下个实例中并没有发生override,你能够定位出是哪些地方出了问题吗?1
2
3
4
5
6
7
8
9
10
11
12
13
14class 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;
};
答案分别为:
- mf1在基类中为const
- mf2在基类中形参为int
- mf3在基类中被限定为左值
- mf4在基类中没有被声明为virtual
鉴于派生类中声明的virtual函数很容易无法覆写基类中对应的函数,并且编译器并不会对未发生override的行为发出警告,因此C++11提供了一种显式的声明方式表示当前函数需要override基类中对应的函数,其使用方式如下:1
2
3
4
5
6
7class 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
14class 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
5class 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
2void 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
8class Widget {
public:
using DataType = std::vector<double>; // see in Item9
DataType& data() { return values; }
…
private:
DataType values;
};
显然这个东西根本不能体现出一丝一毫的封装性,但我们先不管他,看看用户可能进行的操作:1
2
3Widget w;
…
auto vals1 = w.data(); // copy w.values into vals1
由于w.data()返回一个左值引用,因此vals的类型被推衍为vector
假设我们当前有一个工厂函数: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
10class 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;
};
一切大功告成。
总结
- 将需要覆写的函数声明为override。
- reference qualifier可以针对对象的左右值属性实现重载。