C++ Chapter 6. Inheritance and Object-Oriented Design



1. Public inheritance means "is-a." Everything that applies to base classes must also apply to derived classes, because every derived class objectis a base class object.

2.

When compilers see the use of the name mf2 here, they have to figure out what it refers to. They do that by searching scopes for a declaration of something namedmf2. First they look in the local scope (that of mf4), but they find no declaration for anything calledmf2. They then search the containing scope, that of the class Derived. They still find nothing namedmf2, so they move on to the next containing scope, that of the base class. There they find something namedmf2, so the search stops. If there were no mf2 in Base, the search would continue, first to the namespace(s) containingBase, if any, and finally to the global scope.

In the following example, if derived class object call mf5(), it will call the function in base class if there is no mf5() in derived class. Because we inherit a mf5() in base class

class Base {

private:

  int x;



public:

  virtual void mf1() = 0;

  virtual void mf1(int);



  virtual void mf2();



  void mf3();

  void mf3(double);
void mf5();

  ...

};



class Derived: public Base {

public:

  virtual void mf1();

  void mf3();

  void mf4();

  ...

};



This code leads to behavior that surprises every C++ programmer the first time they encounter it. The scope-based name hiding rule hasn't changed, soall functions namedmf1 and mf3 in the base class are hidden by the functions namedmf1 andmf3 in the derived class. From the perspective of name lookup,Base::mf1andBase::mf3 are no longer inherited by Derived!

  • Names in derived classes hide names in base classes. Under public inheritance, this is never desirable. Because it break the "is - a " rule. So we need to use "using"


Derived d;

int x;



...

d.mf1();                   // fine, calls Derived::mf1

d.mf1(x);                  // error! Derived::mf1 hides Base::mf1

d.mf2();                   // fine, calls Base::mf2



d.mf3();                   // fine, calls Derived::mf3

d.mf3(x);                  // error! Derived::mf3 hides Base::mf3


3. 

You do it with using declarations:

class Base {

private:

  int x;



public:

  virtual void mf1() = 0;

  virtual void mf1(int);



  virtual void mf2();



  void mf3();

  void mf3(double);

  ...

};



class Derived: public Base {

public:

  using Base::mf1;       // make all things in Base named mf1 and mf3

  using Base::mf3;       // visible (and public) in Derived's scope



  virtual void mf1();

  void mf3();

  void mf4();

  ...

};



Now inheritance will work as expected:

Derived d;

int x;



...



d.mf1();                 // still fine, still calls Derived::mf1

d.mf1(x);                // now okay, calls Base::mf1



d.mf2();                 // still fine, still calls Base::mf2



d.mf3();                 // fine, calls Derived::mf3

d.mf3(x);                // now okay, calls Base::mf3

This means that if you inherit from a base class with overloaded functions and you want to redefine or override only some of them, you need to include ausing declaration for each name you'd otherwise be hiding. If you don't, some of the names you'd like to inherit will be hidden.

It's conceivable that you sometimes won't want to inherit all the functions from your base classes. You can only achieve this in private inheritance by using forwarding function

class Base {

public:

  virtual void mf1() = 0;

  virtual void mf1(int);



  ...                                    // as before

};



class Derived: private Base {

public:

  virtual void mf1()                   // forwarding function; implicitly

  { Base::mf1(); }                     // inline (see Item

30)

  ...

};



...



Derived d;

int x;



d.mf1();                               // fine, calls Derived::mf1

d.mf1(x);                              // error! Base::mf1() is hidden
:


4. 

Incidentally, it is possible to provide a definition for a pure virtual function. That is, you could provide an implementation forShape::draw, and C++ wouldn't complain, but the only way to call it would be to qualify the call with the class name:

Shape *ps = new Shape;              // error! Shape is abstract



Shape *ps1 = new Rectangle;         // fine

ps1->draw();                     // calls Rectangle::draw



Shape *ps2 = new Ellipse;           // fine

ps2->draw();                     // calls Ellipse::draw



ps1->Shape::draw();                 // calls Shape::draw



ps2->Shape::draw();                 // calls Shape::draw


The two most salient features of pure virtual functions are that theymust be redeclared by any concrete class that inherits them, and they typically have no definition in abstract classes. Put these two characteristics together, and you realize that

The purpose of declaring a pure virtual function is to have derived classes inherit a functioninterface only.

The story behind simple virtual functions is a bit different from that behind pure virtuals. As usual, derived classes inherit the interface of the function, but simple virtual functions provide an implementation that derived classes may override. If you think about this for a minute, you'll realize that

The purpose of declaring a simple virtual function is to have derived classes inherit a functioninterface as well as a default implementation.

When a member function is non-virtual, it's not supposed to behave differently in derived classes. In fact, a non-virtual member function specifies an invariant over specialization, because it identifies behavior that is not supposed to change, no matter how specialized a derived class becomes. As such,

  • The purpose of declaring a non-virtual function is to have derived classes inherit a function interface as well as a mandatory implementation.



5.

The problem here is not that Airplane::fly has default behavior, but that ModelC was allowed to inherit that behavior without explicitly saying that it wanted to. Fortunately, it's easy to offer default behavior to derived classes but not give it to them unless they ask for it. The trick is to sever the connection between the interface of the virtual function and its default implementation. Here's one way to do it:

class Airplane {

public:

  virtual void fly(const Airport& destination) = 0;



  ...



protected:

  void defaultFly(const Airport& destination);

};



void Airplane::defaultFly(const Airport& destination)

{

  default code for flying an airplane to the given destination

}

6.

Different ways to replace public virtual functions:

1.The Template Method Pattern via the Non-Virtual Interface Idiom

Use: we can add do before stuff and do after stuff.

class GameCharacter {

public:

  int healthValue() const               // derived classes do not redefine

  {                                     // this — see Item 36



    ...                                 // do "before" stuff — see below



    int retVal = doHealthValue();       // do the real work



    ...                                 // do "after" stuff — see below



    return retVal;

  }

  ...



private:

  virtual int doHealthValue() const     // derived classes may redefine this

  {

    ...                                 // default algorithm for calculating

  }                                     // character's health

};


This basic design — having clients call private virtual functions indirectly through public non-virtual member functions — is known as the non-virtual interface (NVI) idiom. It's a particular manifestation of the more general design pattern called Template Method (a pattern that, unfortunately, has nothing to do with C++ templates). I call the non-virtual function (e.g., healthValue) the virtual function's wrapper

2. The Strategy Pattern via Function Pointers

Use: 

class GameCharacter;                               // forward declaration



// function for the default health calculation algorithm

int defaultHealthCalc(const GameCharacter& gc);



class GameCharacter {

public:

  typedef int (*HealthCalcFunc)(const GameCharacter&);



  explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)

  : healthFunc(hcf)

  {}



  int healthValue() const

  { return healthFunc(*this); }



  ...



private:

  HealthCalcFunc healthFunc;

};


This approach is a simple application of another common design pattern, Strategy. Compared to approaches based on virtual functions in the GameCharacterhierarchy, it offers some interesting flexibility:

  • Different instances of the same character type can have different health calculation functions. For example:

    class EvilBadGuy: public GameCharacter {
    
    public:
    
      explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
    
      : GameCharacter(hcf)
    
      { ... }
    
    
    
      ...
    
    
    
    };
    
    int loseHealthQuickly(const GameCharacter&);    // health calculation
    
    int loseHealthSlowly(const GameCharacter&);     // funcs with different
    
                                                    // behavior
    
    
    
    EvilBadGuy ebg1(loseHealthQuickly);             // same-type charac-
    
    EvilBadGuy ebg2(loseHealthSlowly);              // ters with different
    
                                                    // health-related
    
                                                    // behavior
    
    
3. 
The Strategy Pattern via tr1::function


we replace the use of a function pointer (such as healthFunc) with an object of type TR1::function. As Item 54 explains, such objects may hold any callable entity(i.e., function pointer, function object, or member function pointer) whose signature is compatible with what is expected. Here's the design we just saw, this time usingtr1::function:

These constraints evaporate if we replace the use of a function pointer (such as healthFunc) with an object of type TR1::function. As Item 54 explains, such objects may hold any callable entity (i.e., function pointer, function object, or member function pointer) whose signature is compatible with what is expected. Here's the design we just saw, this time using tr1::function:

class GameCharacter;                                 // as before

int defaultHealthCalc(const GameCharacter& gc);      // as before



class GameCharacter {

public:

   // HealthCalcFunc is any callable entity that can be called with

   // anything compatible with a GameCharacter and that returns anything

   // compatible with an int; see below for details

   typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;

   explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)

   : healthFunc(hcf)

   {}



   int healthValue() const

   { return healthFunc(*this);   }



   ...



private:

  HealthCalcFunc healthFunc;

};


short calcHealth(const GameCharacter&);          // health calculation

                                                 // function; note

                                                 // non-int return type



struct HealthCalculator {                        // class for health

  int operator()(const GameCharacter&) const     // calculation function

  { ... }                                        // objects

};



class GameLevel {

public:

  float health(const GameCharacter&) const;      // health calculation

  ...                                            // mem function; note

};                                               // non-int return type





class EvilBadGuy: public GameCharacter {         // as before

  ...

};

class EyeCandyCharacter:   public GameCharacter {  // another character

  ...                                              // type; assume same

};                                                 // constructor as

                                                   // EvilBadGuy





EvilBadGuy ebg1(calcHealth);                       // character using a

                                                   // health calculation

                                                   // function





EyeCandyCharacter ecc1(HealthCalculator());        // character using a

                                                   // health calculation

                                                   // function object



GameLevel currentLevel;

...

EvilBadGuy ebg2(                                   // character using a

  std::tr1::bind(&GameLevel::health,               // health calculation

          currentLevel,                            // member function;

          _1)                                      // see below for details

);

We want to say that to calculate ebg2's health rating, the health member function in the GameLevel class should be used. Now, GameLevel::health is a function that is declared to take one parameter (a reference to a GameCharacter), but it really takes two, because it also gets an implicit GameLevel parameter — the one this points to. Health calculation functions for GameCharacters, however, take a single parameter: the GameCharacter whose health is to be calculated. If we're to useGameLevel::health for ebg2's health calculation, we have to somehow "adapt" it so that instead of taking two parameters (a GameCharacter and a GameLevel), it takes only one (a GameCharacter). In this example, we always want to use currentLevel as the GameLevel object for ebg2's health calculation, so we "bind"currentLevel as the GameLevel object to be used each time GameLevel::health is called to calculate ebg2's health. That's what the tr1::bind call does: it specifies that ebg2's health calculation function should always use currentLevel as the GameLevel object.


4. The "Classic" Strategy Pattern

class GameCharacter;                            // forward declaration



class HealthCalcFunc {

public:



  ...

  virtual int calc(const GameCharacter& gc) const

  { ... }

  ...



};



HealthCalcFunc defaultHealthCalc;



class GameCharacter {

public:

  explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)

  : pHealthCalc(phcf)

  {}



  int healthValue() const

  { return pHealthCalc->calc(*this);}



  ...



private:

  HealthCalcFunc *pHealthCalc;

};

7.

  • Use the non-virtual interface idiom (NVI idiom), a form of the Template Method design pattern that wraps public non-virtual member functions around less accessible virtual functions.

  • Replace virtual functions with function pointer data members, a stripped-down manifestation of the Strategy design pattern.

  • Replace virtual functions with tr1::function data members, thus allowing use of any callable entity with a signature compatible with what you need. This, too, is a form of the Strategy design pattern.

  • Replace virtual functions in one hierarchy with virtual functions in another hierarchy. This is the conventional implementation of the Strategy design pattern.


    8.

    The reason for this two-faced behavior is that non-virtual functions like B::mf and D::mf are statically bound (see Item 37). That means that because pB is declared to be of type pointer-to-B, non-virtual functions invoked through pB will always be those defined for class B, even if pB points to an object of a class derived from B, as it does in this example.

    Virtual functions, on the other hand, are dynamically bound (again, seeItem 37), so they don't suffer from this problem. If mf were a virtual function, a call to mfthrough either pB or pD would result in an invocation of D::mf, because what pB and pD really point to is an object of type D.


    9. 

    • Never redefine an inherited default parameter value, because default parameter values are statically bound(bond to the pointer type), while virtual functions — the only functions you should be overriding — are dynamically bound(bond to the object type).

10.

in contrast to public inheritance, compilers will generally not convert a derived class object (such as Student) into a base class object (such as Person) if the inheritance relationship between the classes is private.  Private inheritance means is-implemented-in-terms-of. If you make a class D privately inherit from a class B, you do so because you are interested in taking advantage of some of the features available in class B, not because there is any conceptual relationship between objects of types Band D.


11. 

  • Multiple inheritance is more complex than single inheritance. It can lead to new ambiguity issues and to the need for virtual inheritance.

  • Virtual inheritance imposes costs in size, speed, and complexity of initialization and assignment. It's most practical when virtual base classes have no data.

  • Multiple inheritance does have legitimate uses. One scenario involves combining public inheritance from an Interface class with private inheritance from a class that helps with implementation.

12. 

Derived  mp;

mp.base::checkOut();              // ah, that checkOut...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值