Item 12: Declare overriding functions override

本文讨论了C++中面向对象编程的关键概念,特别是虚函数的重写。强调了重写虚函数时容易出错,并澄清了重写与重载的区别。介绍了C++11引入的`override`关键字,用于明确指定派生类函数是否重写基类的虚函数,从而减少错误并提高代码的清晰性。同时提到了成员函数引用限定符,用于区分左值和右值对象。总结来说,使用`override`关键字能帮助开发者更好地管理和检查虚函数的重写行为。
摘要由CSDN通过智能技术生成

在C++中,面向对象程序设计是以类、继承和虚函数为中心的。其中,最基本的理念是:在派生类中重写从基类继承过来的虚函数。令人遗憾的是,重写虚函数很容易出错。
因为重写(overriding)听起来有点像重载(overloading),但是这两个是完全不相关的。在此,我们澄清,重写虚函数是为了通过基类的接口来调用派生类的函数。

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 =			// create base class pointer
	std::make_unique<Derived>();	// to derived class object;
									// see Item 21 for info on
									// std::make_unique
...

upb->doWork();						// call doWork through base
									// class ptr; derived class
									// function is invoked

为了能够成功达到重写,必须满足一些要求:

  • 基类函数必须是virtual的。
  • 基类函数和派生类函数的名字必须完全一样(除了析构函数)。
  • 基类函数和派生类函数的参数类型必须完全一样。
  • 基类函数和派生类函数的const属性必须完全一样。
  • 基类函数和派生类函数的返回值类型以及异常规格(exception specification)必须是可兼容的。

这些是C++98要求的限制,C++11还增加了一条:

  • 函数的引用限定符必须一致。

“成员函数引用限定符”是C++11中不太被知道的一个特性,所以即使没有听过,也不需要惊讶。它们的出现是为了限制成员函数只能被左值或右值使用。被引用限定符修饰的成员函数,不必是虚函数:

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 &&)

关于成员函数引用限定符的细节,随后再说,现在我们只需要记住,如果基类中的一个虚函数被引用限定符修饰,那么继承类中的重写函数也必须被同样的引用限定符修饰。如若不然,声明的函数还是存在于派生类中的,只是不会重写基类中的任何函数。
为了达到重写的目的,有这么多的要求和限制,这就意味着一个小小的错误,就可以导致很大偏差。包含错误重写的代码常常是有效的,但往往你不是你想要的。因此,你不能依赖编译器提醒你是否有错误。例如,下面的代码就完全是合法的,乍一看还很合理,但一个重写的虚函数都没有(没有一个派生类的函数是关联到基类的函数的)。你能找出每种情况的问题所在么,也就是,为什么每个同名的派生类函数没有重写基类函数?

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
  • mf3在基类中是左值限定的,但是在派生类中是右值限定的。
  • mf4在基类中没声明为virtual的

你可能会觉着,“嘿,实际中,这些东西编译器都会发出警告,所以我不需要去关心它”。这可能是对的,但是也可能是错的。我测试过两个编译器,代码成功被编译器接受,并且编译器没有发出警告,并且这是在警告选项全部打开的情况下测试的。(其他编译器会对其中几条问题产生警告,但也不全)。
由于声明派生类中函数的重写行为很重要,但却极易出错,C++11提供了一种显示来说明派生类函数重写基类函数的方法:加上override声明。把这个一点用于上面的例子,就会产生下面的代码:

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的原因。
使用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;			// adding "virtual" is OK,
	                                    // but not necessary
};

注意,在这个例子中,为了让代码能工作,有一处是将Base中的mf4声明为虚函数。大多数重写相关的错误都是在派生类中,但在基类上出问题也是完全有可能的。
把所有的派生类中的重写函数都声明为override,这个准则不仅能让编译器告诉你什么地方声明了override却没有重写任何东西。而且当你考虑改变基类中虚函数的签名,它(这个准则)还能帮助你评估出波及面。如果派生类中每处都应用了override,你修改了函数签名,然后重新编译,就能看到造成了多大破坏(数一数多少派生类编译失败),然后决定是否值得去修改函数签名。没有override声明,你只能寄希望于有一个完备的单元测试了。
C++ 一直都有关键字,但c++ 11引入了两个上下文相关的关键字: override和final(将final应用于虚函数,会阻止它在派生类中被重写。final也可以修饰一个类,这种情况,该类会被禁止用作基类,也就是无法被继承)。这两个关键字的特点是,只在特定的上下文中它们是保留的(不能用作其他name)。以override为例,只有当它出现在成员函数声明的最后时,它才是保留的。这意味着对于历史遗留的代码,代码中已经使用了override作为函数名,你不需要因为使用了C++11而改变它:

class Warning {           // potential legacy class from C++98
public:void override();      // legal in both C++98 and C++11// (with the same meaning)
};

关于override要说的已经说完了,但是有关成员函数引用限定符的东西还没说完。我之前保证过我会在后面提供有关它们的信息,现在是时候了。
如果我们想写一个函数,这个函数只接受左值参数,我们可以声明一个非const左值引用的参数:

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

如果我们想写一个函数,这个函数只接受右值参数,我们可以声明一个右值引用的参数:

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

成员函数引用限定符的作用就是针对发起成员函数调用的对象(即*this)加以区分。这和在成员函数后面加一个const的情形一样,即表明发起成员函数调用的对象应为const。
对于需要引用限定符修饰的成员函数不常见,但也有可能出现。举个例子,假设我们的Widget类有一个std::vector数据成员,并且我们提供一个访问函数来让客户直接访问这个变量:

class Widget {
public:
	using DataType = std::vector<double>;	// see Item 9 for info on "using"
	...

	DataType& data() { return values; }
	...

private:
	DataType values;
};

这不符合大多数封装设计的标准,但是把它放在一边,并且考虑在下面的客户代码中会发生什么:

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

Widget::data的返回类型是一个左值引用(准确地说是std::vector&),并且因为左值引用被定义为左值,我们用一个左值来初始化vals1。因此,就像注释说的那样,vals1是以w.values进行拷贝构造的。
现在假设我们有一个构建Widget对象的工厂函数,
Widget makeWidget();
并且我们想通过makeWidget返回的Widget,用这个Widget中的std::vector来初始化一个变量:

auto vals2 = makeWidget().data();    // copy values inside the Widget into vals2

同样地,Widget::data返回一个左值引用,并且,同样地,左值引用是一个左值,所以同样地,我们的新对象(vals2)通过拷贝构造函数拷贝了一份Widget中的值。这次Widget是一个从makeWidget返回的临时对象(一个左值,),拷贝它的std::vector浪费时间,我们最好的做法是move它,但是因为data返回一个左值引用,所以C++的规则要求编译器生成拷贝的代码。(若是通过所谓的“准规则”来优化的话,这里有一些回旋余地,但是如果你只能依赖你的编译器找到方法来优化它,那你就真是太蠢了)
我们需要一个方法来明确一点,那就是当data被一个右值Widget调用时,结果也应该是一个右值。使用引用限定符来重载data的左值和右值版本让之成为可能:

class Widget {
public:
    using DataType = std::vector<double>;
    …
    
    DataType& data() &                  // for lvalue Widgets,
    { return values; }                  // return lvalue
    
    DataType data() &&                  // for rvalue Widgets,
    { return std::move(values); }       // return rvalueprivate:
    DataType values;
};

注意两个重载函数的返回值类型不同。左值引用重载函数返回一个左值引用(也就是一个左值),然后右值引用重载函数返回一个临时对象(也就是一个右值)。这意味着现在,客户代码的表现是这样的:

auto vals1 = w.data();               // calls lvalue overload for
                                     // Widget::data, copy-
                                     // constructs vals1
                                     
auto vals2 = makeWidget().data();    // calls rvalue overload for
                                     // Widget::data, move-
                                     // constructs vals2

这确实表现得很好,但是不要让这happy ending的光辉分散了你的注意力,这章的重点是当你在派生类中声明一个函数,并打算用这个函数重写一个基类中的虚函数时,你要把这函数声明为override的。

Things to Remember

  • 把重写函数声明为override的。
  • 成员函数引用限定符能区别对待左值和右值对象(*this)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值