函数重写
在进行本文的协变返回类型(covariant return type)的讨论之前,不妨先重新温故C++关于函数重写的语法规则。协变返回类型与函数重写有着千丝万缕的联系。
首先明确函数重载(overload)与函数重写(override )之间的差异:
函数重载与函数重写
函数重载
同名不同参(不同的参数类型、不同的参数个数、不同的参数顺序)
函数重写
也称为覆盖,主要在继承关系中体现(也即是让子类去重写父类中的虚函数),子类重新定义父类中同名同参的虚函数。
函数重写的条件与注意
基本条件:
派生类重写的函数与基类中被重写的函数必须都是virtual函数,当然子类中的函数重写不必显式声明virtual虚函数。
参数必须保持一致,否则就是子类中自己的成员函数,未构成函数重写。
重写的函数与被重写的函数返回值类型相同,或者当返回指针或者引用时(不包括vaue语义),子类中重写的函数返回的指针或者引用是父类中被重写函数所返回指针或引用的子类型(这就是所谓的covariant return type:协同返回类型)
子类中的函数与父类中的同名函数具有相同的参数和返回值类型时,但如果一个是const函数(non-mutable)、一个是非const函数(mutable),不构成函数重写。
注意:
重写的函数所抛出的异常必须和被重写的函数抛出的异常一致,或者是其子类
重写的函数的访问修饰符可以不同于被重写的函数,如基类中被重写的函数访问修饰符为
private
,派生类中重写的函数的访问修饰符可以为public
或protected
。静态方法不能被重写,因为
static
与virtual
不能同时被使用。子类中重写的函数
virtual
关键字可以加也可以不加, 本质上都是虚函数的属性,只要与基类中的相关函数构成重写关系。
我们以一个具体实例分析C++的函数重写机制吧:
class A{};
class B:public A{};
class C
{
public:
virtual void func1()
{ cout << "C::func1()" << endl;}
virtual A* func2()
{
cout << "C::func2()" << endl;
return new A;
}
virtual A& func3()
{
cout << "C::func3()" << endl;
return *(new A);
// 这里返回的必须是一个堆对象(heap object),
// 而不能是栈对象(stack object)或者叫局部对象
// 因为栈对象会在函数退出时被销毁,皮之不存毛将焉附
// 对象都不存在了,引用自然无所适从
}
void showFunc4()
{
func4();
}
virtual void func5() const
{
cout << "C::func5()" << endl;
}
virtual A func6()
{
cout << "C::func6()" << endl;
}
private:
virtual void func4()
{
cout << "C::func4()" << endl;
}
};
class D:public C
{
public:
// 构成重写
void func1()
{
cout << "D::func1()" << endl;
}
// 返回值的类型是父类返回值类型的子类的指针,构成重写
B* func2()
{
cout << "D::func2()" << endl;
return new B;
}
// 返回值的类型是父类返回值类型的子类的引用,构成重写
B& func3()
{
cout << "D::func3()" << endl;
return *(new B);
}
// 可以对访问修饰符进行重写
void func4()
{
cout << "D::func4()" << endl;
}
// 不构成重写
void func5()
{
cout << "D::func5()" << endl;
}
// 无法通过编译
//B func6()
//{
// cout << "D::func6()" << endl;
//}
};
int main(int, char**)
{
C* c = new D; // 父类指针指向子类对象
c->func1();
c->func2();
c->func3();
c->showFunc4();
c->func5();
return 0;
}
分析这种问题的套路:先判断是否构成重写关系,再看是否是指针或者引用指向的是父类对象还是子类对象,如果是父类指针指向子类对象,且构成重写关系,将会调用子类中的重写函数。
D::func1()
D::func2()
D::func3()
D::func4()
C::func5()
协变返回类型
有了上述的准备,我们便比较容易理解作为C++函数重写机制中的一个小的环节的协变返回类型
了,上述的D::func2(), D::func3()
正是对这一语法机制的运用。
我们来看一个更加具体的例子,一般来说,子类对父类某一函数进行重写,必须具备相同的返回值类型。
class Shape
{
public:
virtual double area() const = 0;
}
class Circle :public Shape
{
public:
float area() const; // 错误,返回值类型不同
}
这一规则对covariant return type
却有所放松,也即,子类对父类的某一函数进行重写时,子类函数的返回值类型可以父类函数返回值类型的子类的指针或者引用类型,语义上要求子类与父类能够构成一种is-a
关系。
class Shape
{
public:
virtual Shape* clone() const = 0;
}
class Circle:public Shape
{
public:
Circle* clone() const;
}
重写的派生类函数被声明为返回一个Circle*
而不是继续Shape*
,因为语义上,Circle是一个Shape。这在什么情况下会产生便捷性呢?直接操纵派生类类型而不是通过基类接口来操纵它们时,协变返回类型的优势便会体现出来:
Circle* c = getACircle();
Circle* c2 = c->clone();
c2->someFunc();
// 如此不经过类型转换我们便可实现对子类函数的调用
如果不使用协变返回类型机制,或者不存在协变返回类型机制,Circle::clone将不得不精确地匹配Shape::clone的返回类型,从而返回一个Shape*,我们就必须被迫将结果转换为Circle*。
Circle* c = getACircle();
Circle* c2 = dynamic_cast<Circle*>(c->clone());
再看另外一个例子,考虑Shape中的Factory Method成员,它返回一个引用,指向与具体的形状对应的形状编辑器:
class ShapeEditor{};
class Shape
{
public:
const ShapeEditor& getEditor() const = 0;
// Factory method
};
class CircleEditor:public ShapeEditor{};
class Circle: public Shape
{
public:
const CircleEditor& getEditor() const {}
}
协变返回类型的最大或者说根本优势在于,让程序员在适度程度的抽象层面工作。如果我们是处理Shape,将获得一个抽象的ShapeEditor
;若正在处理某种具体的形状类型,比如Circle,我们便可直接获得CircleEditor
。协变返回类型将使我们从这样的一种处境中解脱出来:不得不使用易于出错的动态类型转换操作(dynamic_cast
Shape* s = getAShape();
const ShapeEditor& se = s->getEditor();
Circle* c = getACirle();
const CircleEditor& ce = c->getEditor();
一个协变返回类型的实例
enum AttrType { Unknown, Continuous, Discrete};
class AttrInfo
{
public:
AttrInfo(const std::string& name, AttrType type)
:_type(type), _name(name)
{}
virtual AttrInfo* clone() const = 0;
private:
AttrType _type;
std::string _name;
};
class CAttrInfo :public AttrInfo
{
public:
// ...
CAttrInfo* clone() const
{
return (new CAttrInfo(*this));
}
// ...
}
class DAttrInfo :public AttrInfo
{
public:
// ...
DAttrInfo* clone() const
{
return (new DAttrInfo(*this));
}
// ...
}
如上述代码所示,父类(抽象基类)中定义的被重写的函数返回类型是AttrInfo*
(父类类型的指针),而两个派生类各自对clone
函数的重写版本返回类型为CAttrInfo*
和DAttrInfo*
,因为有了协变返回类型(covariant return type)的存在,这样的语法特性才得以支持。
重定义(redefining)
concept
也叫隐藏,子类重新定义父类中的非虚函数,屏蔽了父类的同名函数。
基本条件
函数与被其隐藏的函数作用域不同,也就是函数在子类中声明,被其隐藏的函数在父类中声明。
note
- 同名,不同参,无论父类的函数是否为virtual,都将被隐藏
- 同名,同参,如果父类的函数不是virtual,将被隐藏
References
[1] <C++重载、重写、重定义区别>
[2] <协变返回类型>