Effective Modern C++ 条款12 把重写函数(overriding function)声明为override

来源:互联网 发布:新浪汽车销量数据库 编辑:程序博客网 时间:2024/06/01 07:43

把重载函数声明为override

在C++面向对象编程的世界中,会反复出现类,继承,虚函数。在那个世界最基础思想就是派生类重写(override)基类的虚函数。意识到重写虚函数容易出错误是件令人沮丧的事情,C++中重写函数是不遵守墨菲定理,这是值得尊敬的。

虽说“overriding”(重写)听起来像“overloading”(重载),不过它们完全没有关系,我们需要清楚虚函数重写是让派生类用基类的接口定义自己的函数:

class Base {public:    virtual void doWork();   //  基类的虚函数    ...};class  Derived: public Base {public:    virtual void doWork();   // 重写 Base::doWork()     ...                        // “virtual”关键字可有可无};// 创建一个指向Derived对象的Base指针,std::make_unique的解释看条款21std::unique_ptr<Base> upb = std::make_unique<Derived>();...upb->doWork();   // 通过Base指针调用doWork,使用的是Derived类的函数

如果你想要用重写函数,一些要求也随之而来:

  • 基类的函数必须是虚函数(virtual)。
  • 基类和派生类的函数名字必须相同(除了析构函数)。
  • 基类和派生类的函数参数必须相同。
  • 基类和派生类的const属性必须相同
  • 基类和派生类的返回类型和异常规范(exception specifications)必须兼容。

这些约束是C++98的一部分,C++11增加了一条:

  • 函数的引用资格(reference qualifiers)必须相同。

成员函数的引用资格是C++11中少有宣传的新特性,所以从来没听过它也不用感到惊讶。它们可以限制成员函数只能由左值使用,或者只能由右值使用,而且这些成员函数无需声明虚函数:

class Widget {public:    ...    void doWork() &;    // 当*this是左值时才能使用这个版本的doWork    void doWork() &&;  // 当*this是右值时才能使用这个版本的doWork};...Widget makeWidget();   //  工厂函数(返回右值)Widget w;    // 普通对象(左值)...w.doWork();  // 调用doWork的左值版本( Widget::dowork() & )makeWidget().doWork();  // 调用doWork的右值版本( Widget::dowork() && )

我等下会讲多点带引用资格成员函数的内容,但是现在,你只需知道如果基类虚函数中标记了引用资格,那么派生类的重新函数也必须要有相同的引用资格。如果不是这样,声明的函数虽然还是存在却没有重写任何基类函数。

所有这些重写函数的要求,意味着一个小小的错误也会引起巨大的不同。通常情况下,代码中含有重新函数的错误也是可运行的,不过它并非如你所愿,所以你不能指望编译器提示你做错了些什么。例如,下面的代码是完全合法的,第一眼看上去没问题,但它没有重写虚函数——派生类没有一个函数关系到基类函数。为什么派生类中的函数都没有重写到基类的函数,你能找到其中的原因吗?

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;};

需要帮助吗?

  • 基类的mf1声明为const,但派生类没有。
  • 基类的mf2参数类型是int,但派生类是unsigned int
  • 基类的mf3是左值资格,但派生类是右值资格。
  • 基类的mf4不是虚函数。

你可能会想,在实践中这些东西将会引出编译器警告,所以不用担心。可能你是对的,但可能你是错的。在我测试的两个编译器中,编译器没发出任何警告。

在派生类中把重写函数声明正确是重要的,但又容易出错误,所以C++11提供了一种把派生类函数显示声明为重写函数的方法:声明为override。在上面的例子中使用它:

class Derived: public Base {public:    virtual void mf1() override;    virtual void mf2(unsigned int x) override;    virtual void mf3() && override;    void mf4() const override;};

当然,这是不能通过编译的,因为这样写时编译器会发出一堆关系到重写函数的问题。这就是你想要的,也就是为什么应该把所有的重写函数声明为override

使用override的可编译通过的代码是这样的(假设派生类所有函数的目的是重新基类的虚函数):

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;  // “virtual”不是必要的};

在这个代码中,我们修改了基类的mf4,大部分重写函数错误都是发生在派生类,但也有可能错误是出在基类。

在派生类重写方法使用override不只是使能编译器告诉你应该重写的函数,当你考虑修改基类虚函数的签名时,它还能帮助你评估这个行为的后果。如果派生类函数使用了override,你可以直接修改签名,重新编译你的代码,看看你会造成多少伤害(有多少个派生类编译失败),然后再决定修改签名是否值得。如果没有override,你就要在相应的地方广泛地单元测试,因为,如我们所见,派生类可以重写基类虚函数,但也可以不重写,此时编译器不会帮你诊断。

C++一直都有关键字,但C++11引入了两个上下文关键字(contextual keyword),overridefinal。这两个关键字是被保留的(即不能作为变量什么的使用),不过只是在确定的上下文。例如,override只有在它出现在成员函数声明的最后才有保留意义。这意味着你的旧代码中使用了override这个名字,你在C++11中无需改变它们:

class Widget {   // C++98时的旧代码public:    ...   void override();   // 在C++98和C++11中都合法,且意义一样    ...};

关于override的内容已经说完了,不过关于成员函数引用资格还有话说。我承诺过等下会讲多点它的内容,而现在就是那个等下。

如果我们想要写一个只接收左值作为参数的函数,那么我们可以把参数声明为非const
void doSomething(Widget& w); // 只接收左值Widget

如果我们想要写一个只接收右值作为参数的函数,那么我们可以把参数声明为右值引用:
void doSomething(Widget&& w); // 只接受右值Widget

成员函数引用资格与上面相同,简单地区别调用成员函数的对象的左值和右值,也就是*this。它与把const放在成员函数声明的末尾十分相似,表面调用成员函数的对象(*this)是const性质。

需要引用资格的成员函数不常见,但还是有的。例如,我们有个Widget类,它有个成员变量std::vector,然后我们提供一个获取函数来让用户直接获取它:

class Widget {public:    using DataType = std::vector<double>;   // 关于using,看条款9    ...    DataType& data() { return values; }    ...private:    DataType values;};

这不是完全封装,漏光了,但把那些放到一边先考虑这样的用户代码:

Widget w;...auto vals1 = w.data();  // 把w.values拷贝到vals1

Widget::data的返回类型是个左值引用,因为左值引用也是个左值,所以我们用一个左值初始化vals1,vals1通过w.values进行拷贝构造,就如注释里所说。

现在假设我们有个工厂函数来创建Widget:
Widget makeWidget();

然后现在我们想要要makeWidget返回的Widget中的std::vector来初始化变量:
auto vals2 = makeWidget().data();

再一次,Widget::data返回左值引用,左值引用也还是左值,,所以我们的新对象(vals2)也是通过Widget中的values进行拷贝构造。不过这次,makeWidget返回的Widget是一个临时对象(它是右值),所以拷贝临时对象中的std::vector有点浪费时间,直接移动它更好,但是,data成员函数返回的是左值引用,C++的规则是让编译器调用拷贝构造。

我们需要指定,当data成员函数被右值Widget调用时的方式,返回的结果也应该是右值。使用引用资格来重载(overload)data函数:

class Widget {public:    using DataType = std::vector<double>;    ...    DataType& data() &  // 左值Widget调用,返回左值    { return values; }    DataType data() &&   // 右值Widget调用,返回右值    { return std::move(values); }    ...private:    DataType values;};

注意重载data函数的不同返回类型。左值引用资格的重载函数返回一个左值引用(它是个左值),右值引用资格的重载函数返回一个临时对象(它是个右值),这意味着用户的代码可以如我们想象那样了:

auto vals1 = w.data(); // 调用左值引用资格的data函数                         // vals1进行拷贝构造auto vals2 = makeWidget().data();  // 调用右值引用资格的data函数                                    // vals2进行移动构造

这结果就很nice啦,不过请你不要忘记本条款的真正用意。它就是:无论何时,若派生类中声明的函数是想要重写(override)基类的虚函数,请确保把那个函数声明为override

总结

需要记住的2点:

  • 把重新函数声明为override(Declare overring functions override)。
  • 成员函数引用资格(reference qualifiers)使区别对待左值和右值对象(*this)成为可能。
0 0
原创粉丝点击