Item 37:継承されたデフォルトパラメータ値は再定義されません

10226 ワード

Item 37:継承されたデフォルトパラメータ値は再定義されません
Item 37:Never redefine a function’s inherited default parameter value
この条項の議論は、デフォルトのパラメータ値を持つvirtual関数を継承することに限定されます.
本条項が成立した理由は、virtual関数はダイナミックバインド(dynamically bound)であり、デフォルトパラメータは静的バインド(statically bound)である.
静的バインドは前期バインドとも呼ばれ、early binding;ダイナミックバインドは後期バインド、late bindingとも呼ばれます.
1.静的タイプと動的タイプ
オブジェクトの静的タイプ(static type)とは、プログラムで宣言されるときに使用されるタイプです.オブジェクトの動的タイプ(dynamic type)とは、「現在のオブジェクトのタイプ」を指します.つまり、動的タイプは、オブジェクトがどのような動作をするかを示すことができます.
例を挙げます.
class Shape {
public:
    enum ShapeColor {Red, Green, Blue};
    //             ,      
    virtual void draw(ShapeColor color = Red) const = 0;
    ...
};

class Rectangle: public Shape{
public:
    //  ,          。    
    virtual void draw(ShapeColor color = Green) const;
    ...
};

class Circle: public Shape{
public:
    virtual void draw(ShapeColor color) const;
    ...
};

この継承体系図は以下の通りである.
これらのポインタを考慮します.
Shape* ps;                     //     Shape* 
Shape* pc = new Circle;        //     Shape*
Shape* pr = new Rectangle;     //     Shape*

ps,pc,prはpointer-to-Shapeタイプとして宣言されているので,いずれもShape*を静的タイプとしている.実際に何を指しているかにかかわらず、静的タイプはShape*です.PCのダイナミックタイプはCircle*,prのダイナミックタイプはRectangle*である.psにはダイナミックタイプはありません.オブジェクトがまだ指定されていないためです.
ダイナミックタイプは、プログラムの実行中に変更できます(通常は、割り当てによって動作します):
ps  =  pc;    //ps        Circle*
ps  =  pr;    //ps        Rectangle*

virtual関数は、動的バインドに基づいています.virtual関数を呼び出すときに、どの実装コードを呼び出すかは、そのオブジェクトを呼び出す動的タイプによって異なります.
pc->draw(Shape::Red);    //  Circle::draw(Shape::Red)
pr->draw(Shape::Red);    //  Rectangle::draw(Shape::Red)

もちろん、これは基礎知識です.君はもう把握しているに違いない.しかし、virtual関数はダイナミックバインドであり、デフォルトのパラメータ値は静的バインドである.「derived class内に定義されたvirtual関数を呼び出す」と同時にbase classで指定したデフォルトパラメータ値を使用する可能性があります.
pr->draw( );      //  Rectangle::draw(Shape::Red

prのダイナミックタイプはRectangle*なので、予想通りRectangleのvirtual関数が呼び出されます.Rectangle::draw関数のデフォルトパラメータ値はGREENであるべきであるが、prの静的タイプはShape*であるため、呼び出されたデフォルトパラメータ値はRectangle classではなくShape classから来ている!結局、この関数呼び出しは奇妙で、ほとんど予想されていなかった組み合わせで、Shape classとRectangle classのdraw宣言でそれぞれ半分の力を出した.
本の中の著者が与えたこの例は、明らかではなく、少し暗くて、上記を理解するのは少し骨が折れるかもしれませんが、例コードを変えましょう.
class Shape {
public:
    virtual void draw(int color = 0) const = 0;
};
class Rectangle: public Shape{
public:
    virtual void draw(int color = 1) const
    {
        cout << "Rectangle class : " << color << endl;
    }
};
class Circle: public Shape{
public:
    virtual void draw(int color) const
    {
        cout << "Circle class : " << color << endl;
    }
};

int main()
{
    Shape* pc = new Circle;        //     Shape*
    Shape* pr = new Rectangle;     //     Shape*
    pc->draw();                    //Circle class : 0
    pr->draw();                    //Rectangle class : 0
    pc->draw(10);                  //Circle class : 10
    pr->draw(10);                  //Rectangle class :10
    return 0;
}

これで皆さんはきっとよく知っているでしょう.あまり説明しません.
以上の事実は「ps,pc,prともにポインタ」の場合に限らない.ポインタをreferencesに変えても問題は残っています.
Circle c;
Rectangle r;
Shape &sc = c;
Shape &sr = r;
sc.draw();                    //Circle class : 0
sr.draw();                    //Rectangle class : 0
sc.draw(10);                  //Circle class : 10
sr.draw(10);                  //Rectangle class :10

なぜC++はこのような方法で動作しているのでしょうか.
答えは稼働期間の効率です.デフォルトのパラメータ値がダイナミックバインドの場合、コンパイラは実行中にvirtual関数に適切なパラメータデフォルト値を決定する方法が必要です.これは,現在実行されている「コンパイル期間で決定する」メカニズムよりも遅く複雑である.プログラムの実行速度とコンパイラの実現上の簡易度のために、C++はこのような取捨選択をして、その結果、あなたが今享受している実行効率です.
コード繰返し問題
このルールを遵守し、baseとderived classのユーザーにデフォルトのパラメータを同時に提供してみると、何が起こるのでしょうか.
class Shape {
public:
    enum ShapeColor {Red, Green, Blue};
    virtual void draw(ShapeColor color = Red) const = 0;
    ...
};
class Rectangle: public Shape{
public:
    virtual void draw(ShapeColor color = Red) const;
    ...
};

コードが重複しているように見えますが、警告も間違っていません.コードが重複して書かれているだけです.さらに悪いことに、コードの重複は依存性を持っています.Shape内のデフォルトパラメータ値が変更された場合、すべての「指定されたデフォルトパラメータ値を繰り返す」derived classesも変更する必要があります.そうでなければ、最終的には、「継承されたデフォルトのパラメータ値を繰り返し定義する」ことが、私たちのこのルールの願いに反します.どうすればいいですか.
virtual関数にあなたが望んでいる行為を表現させたいが、トラブルに遭遇した場合、賢い方法は代替設計を考えることです.条項35には、base class内のpublic non-virtual関数にprivate virtual関数を呼び出し、derived classesによって再定義できるNVI(non-virtual interface)手法が多く記載されています.ここでは、non-virtual関数にデフォルトパラメータを指定し、private virtual関数は真の作業を担当します.
class Shape {
public:
    enum ShapeColor {Red, Green, Blue};
    void draw(ShapeColor color = Red) const         //    non-virtual
    {
        doDraw(color);                                 //    virtual
    }
    ...
private:
    virtual void doDraw(ShapeColor color) const = 0;    //          
};
class Rectangle: public Shape{
public:
    ...
private:
    virtual void doDraw(ShapeColor color) const;        //  ,         。
    ...
};

NOTE:
デフォルトのパラメータ値は静的バインドであり、virtual関数は上書きすべき唯一のものであり、動的バインドであるため、継承されたデフォルトのパラメータ値を再定義しないでください.
ps:本条項は『C++Primer』第4版中国語版第15.2.4節の5点目--「虚関数とデフォルトの実参」に説明があります.P 482.しかし、例を挙げて説明していないので、理解しにくいか、印象が悪いかもしれません.
質問:
#include <iostream>
#include <string>
using namespace std;

 class Base {
 public:
     virtual ~Base(){}
     virtual void display() const {
         cout << "Base" << endl;
     }
 };

 class Derive:public Base {
 public:
    virtual ~Derive(){}
    virtual void display() {
        cout << "Derive" << endl;
    }

 };

 int main()
 {
    Base *pbase = new Derive();
    pbase -> display();
    return 0;
 }

なぜ出力しますか?Base
回答:
この問題は難しくありません.肝心なのは、重荷重、隠し、カバーの違い、constの概念を明らかにしていないことです.
リロード:関数名が同じで、パラメータリストが異なる必要がありますが、2つの関数が同じ役割ドメインにある必要があります.あなたのこのテーマのdispaly関数は、1つはサブクラスで、1つは親クラスで、明らかに同じ役割ドメインではありません.したがって、重荷を排除します.
上書き:親子の間で発生し、親の関数にvirtual修飾が必要になります.また、戻りタイプが基本タイプの関数については、上書きを構成するには、関数名、パラメータリスト、戻り値タイプがすべて同じでなければなりません.あなたの親のdisplayの後ろにconst修飾があります.彼はパラメータリストのthisポインタを修飾します.つまりdispaly(const this).このように書くと、あなたは理解しているでしょう.サブクラスのdisplayはdispaly()であるべきで、明らかにパラメータリストが異なるので、自然に上書きできないでしょう.
≪非表示|Hide|ldap≫:親子クラス間で発生し、関数名が同じであれば非表示になります.
上記のように、あなたのこの2つの関数は、上書きではなく隠し関係を構成しています.上書きではない以上、多態は何ですか.だから運行結果はベースに違いない.
マルチステートを実装するには、2つの方法があります.-親display関数の後のconstを削除します.-子display関数の後にconstを追加します.