C++基礎::語法特性::函式重寫(override)與協變返回型別(covariant return type)

Inside_Zhang發表於2015-11-10

函式重寫

在進行本文的協變返回型別(covariant return type)的討論之前,不妨先重新溫故C++關於函式重寫的語法規則。協變返回型別與函式重寫有著千絲萬縷的聯絡。

首先明確函式過載(overload)與函式重寫(override )之間的差異:

函式過載與函式重寫

  • 函式過載

    同名不同參(不同的引數型別、不同的引數個數、不同的引數順序)

  • 函式重寫

    也稱為覆蓋,主要在繼承關係中體現(也即是讓子類去重寫父類中的虛擬函式),子類重新定義父類中同名同參的虛擬函式。

函式重寫的條件與注意

基本條件:

  1. 派生類重寫的函式與基類中被重寫的函式必須都是virtual函式,當然子類中的函式重寫不必顯式宣告virtual虛擬函式。

  2. 引數必須保持一致,否則就是子類中自己的成員函式,未構成函式重寫。

  3. 重寫的函式與被重寫的函式返回值型別相同,或者當返回指標或者引用時(不包括vaue語義),子類中重寫的函式返回的指標或者引用是父類中被重寫函式所返回指標或引用的子型別(這就是所謂的covariant return type:協同返回型別)

  4. 子類中的函式與父類中的同名函式具有相同的引數和返回值型別時,但如果一個是const函式(non-mutable)、一個是非const函式(mutable),不構成函式重寫。

注意:

  1. 重寫的函式所丟擲的異常必須和被重寫的函式丟擲的異常一致,或者是其子類

  2. 重寫的函式的訪問修飾符可以不同於被重寫的函式,如基類中被重寫的函式訪問修飾符為private,派生類中重寫的函式的訪問修飾符可以為publicprotected

  3. 靜態方法不能被重寫,因為staticvirtual不能同時被使用。

  4. 子類中重寫的函式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] <協變返回型別>

相關文章