item 12:書き換え関数を「override」と宣言する

8062 ワード

本文はmodern effective C++から翻訳して、レベルが限られているため、翻訳が完全に正しいことを保証することができなくて、間違いを指摘することを歓迎します.ありがとう!
C++のオブジェクト向けプログラミングは、常にクラス、継承、および虚関数の周りにあります.この世界で最も基礎的な概念は,1つの虚関数に対して,派生クラスの実装でベースクラスの実装を書き換えることである.しかし、これはがっかりさせられます.虚関数を書き換えるのがどんなに間違いやすいかを認識しなければなりません.これはこの言語のように、このような概念(マーフィーの法則は守られるだけでなく、尊敬される必要がある)で設計されています.(it's almost as if this part of the language were designed with the idea that Murphy's Law wasn't just to be obeyed, it was to be honored)
「書き換え」は「リロード」に似ているように聞こえますが、彼らはまったく関係ありません.仮想関数を書き換えるのは、ベースクラスのインタフェースを通じて派生クラスの関数を呼び出すためであることを明らかにしましょう.
class Base {
public:
    virtual void doWork();          //     
    ...
};

class Derived: public Base{
public:
    virtual void doWork();          //  Base::doWork
    ...                             //(“virtual”     )
};

std::unique_ptr<Base> upb =         //      ,   
    std::make_unique<Derived>();    //     ;  
                                    //std::make_unique   
                                    //  Item 21
...

upb->doWork();                      //        doWork;
                                    //          

書き換えを成功させるには、いくつかの要件を満たす必要があります.
  • ベースクラス関数はvirtualでなければなりません.
  • ベースクラス関数と派生クラス関数の名前は完全に同じでなければなりません(構造関数を除く).
  • ベースクラス関数と派生クラス関数のパラメータタイプは完全に同じでなければなりません.
  • ベースクラス関数と派生クラス関数のconst属性は完全に同じである必要があります.
  • ベースクラス関数と派生クラス関数の戻り値タイプ、および例外仕様(exception specification)は互換性がある必要があります.

  • これらの制限はC++98で要求され、C++11はさらに1つ追加された.
  • 関数の参照修飾子は、
  • と完全に同じでなければならない.
    「メンバー関数参照修飾子」はC++11ではあまり知られていない特性なので、聞いたことがなくても驚く必要はありません.メンバー関数が左または右のいずれかにのみ使用されることを制限するために表示されます.これらを使用する場合は、virtualメンバー関数とは限りません.
    class Widget {
    public:
        ...
        void doWork() &;            //  *this    ,    
                                    //     doWork
    
        void doWork() &&;           //  *this    ,    
                                    //     doWork
    };
    
    ...
    
    Widget makeWidget();            //    (      )
    
    Widget w;                       //     (    )
    
    ...
    
    w.doWork();                     //       Widget::doWork
                                    //   Widget::doWork &
    
    makeWidget().doWork();          //       Widget::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;
    };

    何かお手伝いしましょうか.
  • mf 1はベースクラスでconstと宣言するが、派生クラスでは
  • ではない.
  • mf 2のベースクラスでのパラメータタイプはintであるが、派生クラスでのパラメータタイプはunsigned
  • である.
  • mf 3はベースクラスでは左値限定であるが、派生クラスでは右値限定である.
  • mf 4ベースクラスでvirtualとして宣言されていない
  • 「もしもし、練習中はコンパイラが警告するので、気にする必要はありません」と思っているかもしれません.これは正しいかもしれませんが、間違っているかもしれません.2つのコンパイラをテストしましたが、コードはコンパイラに受け入れられ、コンパイラは警告を発行せず、警告オプションがすべて開いている場合にテストされました.(他のコンパイラは、いくつかの問題(すべてではない)に警告します.)
    派生クラスでは、正しい書き換え関数を宣言することが重要ですが、常にエラーが発生しやすいため、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;              //  “virtual”   ,      
    };

    この例では、Baseでmf 4をvirtualと宣言したことの一部を覚えておいてください.書き換えに関するエラーの大部分は派生クラスで発生するが,ベースクラスに誤りがある可能性もある.
    すべての派生クラスの書き換え関数をoverrideとして宣言します.この準則は、コンパイラにoverrideを宣言した場所を教えてくれるだけでなく、何も書き換えていません.また、ベースクラスの虚関数の署名を変更することを考慮すると、影響が大きいかどうかを評価するのに役立ちます.派生クラスのすべての場所でoverrideが使用されている場合は、関数署名を変更してから、システムを再コンパイルし、どれだけの損害を与えたか(つまり、各派生クラスでコンパイルできない関数がどれだけあるか)を見て、これらの問題が関数署名を変更する価値があるかどうかを決定します.overrideがなければ、全面的なユニットテストがあることを祈るしかありません.なぜなら、私たちが見たように、派生クラスの虚関数はベースクラスの関数を書き換える必要がありますが、「書き換えに成功した」場合、コンパイラは警告を発行しません.
    C++は常にキーワードであるキーワードがありますが、C++11はコンテキストに関連する2つのキーワード、override、finalを紹介しています.この2つのキーワードの特徴は、特定のコンテキストでのみ保持されていることです(他のnameとして使用できません).たとえばoverrideの場合、メンバー関数宣言の最後に表示される場合にのみ保持されます.これは、履歴に残っているコードがある場合、コードにoverrideがnameとして使用されている場合、C++11を使用して変更する必要はありません.
    class Warning {
    public:
        ...
        void override();            // C++98 C++11    
        ...                         //        
    };

    オーバーライドについてはもう話しましたが、メンバー関数については限定子を参照するものはまだ話していません.私は前に私が後でそれらに関する情報を提供することを保証したことがあります.それから今は「後ろ」です.
    関数を書きたい場合、この関数は左のパラメータのみを受け入れます.const以外の左の参照パラメータを宣言できます.
    void doSomething(Widget& w);        //        Widget

    関数を書きたい場合、この関数は右のパラメータのみを受け入れます.右のパラメータを参照するパラメータを宣言できます.
    void foSomething(Widget&& w);       //        Widget

    メンバー関数参照制限子は、異なるオブジェクト(*thisが左に属するか右に属するか)が異なるメンバー関数を呼び出すように区別することもできます(overrideは加算されません).これは、メンバー関数の宣言にconst(constオブジェクトが呼び出すメンバー関数を表す)を付けるのとほぼ同じです.
    限定機能を参照するメンバー関数は一般的ではありませんが、存在します.たとえば、Widgetクラスにstd::vectorデータメンバーがあり、この変数に直接アクセスできるアクセス関数を提供します.
    class Widget {
    public:
        using DataType = std::vector<double>;       //using       Item 9
        ...
    
        DataType& data() { return values; }
        ...
    
    private:
        DataType values;
    };

    これはほとんどのパッケージ設計の基準に合致しませんが、それを片側に置いて、次のクライアントコードで何が起こったのかを考えてみましょう.
    Widget w;
    ...
    
    auto vals1 = w.data();                  // w.values   vals1 

    Widget::dataの戻りタイプは左値参照(正確にはstd::vector&)であり、左値参照が左値として定義されているため、vals 1の初期化は左値から来ている.したがって,注釈で述べたようにw.valuesコピーでvals 1を構築した.
    Widgetを作成できるファクトリ関数があるとします
    Widget makeWidget();

    makeWidgetで返されるWidgetは、このWidgetのstd::vectorで変数を初期化します.
    auto vals2 = makeWidget().data();       // Widget      vals2 

    同様に、Widget::dataは左値参照を返し、同様に左値参照は左値であるため、同様に、我々の新しいオブジェクト(vals 2)はコピー構造関数によってWidgetの値をコピーした.今回のWidgetはmakeWidgetから返される一時オブジェクト(左の値)であり、そのstd::vectorは時間を浪費し、moveが最善の方法であるが、dataは左の値参照を返すため、C++のルールはコンパイラにコピーのコードを生成するように要求する.(いわゆる「as if rule」で最適化すれば、ここにはいくつかの旋回の余地がありますが、コンパイラに頼って方法を見つけて最適化するしかないなら、あなたは本当に愚かです)
    データが右の値Widgetによって呼び出されると、結果も右の値になるべきであることを明確にする方法が必要です.参照制限子を使用してdataの左と右のバージョンを再ロードします.
    class Widget {
    public:
        using DataType = std::vector<double>;
        ...
    
        DataType& data()&           //  Widget    
        { return values;}
    
        DataType data() &&          //  Widget    
        { return std::move(values); }
        ...
    
    private:
        DataType values;
    };

    2つのリロード関数の戻り値のタイプが異なることに注意してください.左参照リロード関数は、左参照(つまり左)を返し、右参照リロード関数は一時オブジェクト(つまり右)を返します.これは、顧客コードの表現が次のようになっていることを意味します.
    auto vals1 = w.data();              //  Widget::data   
                                        //    ,      vals1
    
    auto vals2 = makeWidget().data();   //  Widget::data   
                                        //    ,      vals2

    これは確かによく表現されていますが、このhappy endingの輝きに注意力を分散させないでください.この章の重点は、派生クラスで関数を宣言し、この関数でベースクラスの虚関数を書き換えるつもりでいるときに、この関数をoverrideと宣言することです.
    君が覚えていること
  • 書き換え関数をoverrideと宣言します.
  • メンバー関数参照修飾子は、左値オブジェクトと右値オブジェクトを区別できます(*this).