Effective C++ Reading Notes

Keywords: Programming less

Effective C++ Reading Notes (6)

6. Inheritance and Object-Oriented Design

Clause 32. Determine your public's inheritance to model the is-a relationship

  1. In object-oriented programming in C++, one of the most important rules is that public inheritance means "is-a" (a) relationship.Here is a literal translation, for example, class D: public B literal translation means D is a B.
  2. "public" inheritance means is-a.Everything that applies to base classes must also apply to derived, because each derived object is also a base class object.

Clause 33. Avoid masking inherited names

  1. For variables and functions, the name of the subclass masks the name of the parent class, even if the function is overloaded

     class Base{
        private:
            int x;
        public:
            virtual void mf1()=0;
            virtual void mf1(int);//heavy load
            virtual void mf2();
            void mf3();
            void mf3(double);//heavy load
            ……
        };
        class Derived: public Base{
        public:
            virtual void mf1();
            void mf3();
            void mf4();
            ……
        };
    
     Derived d;
        int x;
        d.mf1();//Correct, call Derived::mf1
        d.mf1(x);//Error because Derived::mf1 masks Base::mf1
        d.mf2();//Correct, call Base::mf2
        d.mf3();//Correct, call Derived::mf3
        d.mf3(x);//Error because Derived::mf3 masks Base::mf3
    

Because of the scope-based Name Masking Rules, all functions named MF1 and MF3 in the base class are masked by the MF1 and MF3 functions in the derived class.From the name lookup point of view, Base::mf1 and Base::mf3 are no longer inherited by Derived.

  1. To avoid this problem, you can use using declarations or forwarding functions in subclasses.For example:

    1. Use using declaration; will inherit all

       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:
              //Make everything in the Base class named mf1 and mf3 visible within the Derived scope and public
              using Base::mf1;
              using Base::mf3;
              virtual void mf1();
              void mf3();
              void mf4();
              ……
          };
      

      Then there's nothing wrong with the above

       Derived d;
          int x;
          d.mf1();//Correct, call Derived::mf1
          d.mf1(x);//Call Base::mf1
          d.mf2();//Correct, call Base::mf2
          d.mf3();//Correct, call Derived::mf3
          d.mf3(x);//Call Base::mf3
      
    2. Exchange function

      Simply inheriting the parameterless version of mf1 within Base requires a simple swap function

          class Base{
          public:
              virtual void mf1()=0;
              virtual void mf1(int);
          };
          class Derived: private Base{
          public:
              virtual void mf1()//forwarding function
              {Base::mf1();};//Implicitly become inline
          };
          Derived d;
          int x;
          d.mf1();//Call Derived::mf1
          d.mf1(x);//Error, Base::mf1 is obscured
      

Clause 34: Distinguish between interface inheritance and implementation inheritance

  1. Pure virtual functions are designed to allow inheritance function interfaces of subclasses, regardless of how they are implemented
  2. Non-pure virtual functions are designed to allow subclasses to optionally implement this interface, or the default implementation of the parent class
  3. Membership functions are designed to allow subclasses to inherit this function and its implementation without changing it (all subclasses use this same implementation)

Clause 35: Consider alternatives to the virtual function (come back after learning design patterns)

  1. Using the non-virtual interface (NVI) technique, which is a design pattern called the template method pattern, uses member functions to wrap virtual functions.

     class GameCharacter{
        public:
            int healthValue() const
            {
                ……  //Work before you do something
                int retVal=doHealthValue();//Do real work
                ……  //Work after doing things
                return retVal;
            }
            ……
        private:
            virtual int doHealthValue() const //derived classes can be redefined
            {
                ……
            }
        };
    
  2. Replace the virtual function with a member variable of the function pointer.Application of Strategy Design Mode

        class GameCharacter;//forward declaration
        int defaultHealthCalc(const GameCharacter& gc);//Health Computing Default Algorithms
        class GameChaaracter{
        public:
            typedef int(*HealthCalcFunc)(const GameCharacter&);
            explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc)
                :healthFunc(hcf)
            {}
            int healthValue()const
            { return healthFunc(*this); }
            ……
        private:
            HealthCalcFunc healthFunc;//Function Pointer
        };
    
    • Different entities of the same person type can have different health calculation functions.That is, objects of the same character type can have different health calculations, for example, in shooting games, some players who buy body armor use objects that can reduce blood volume more slowly.
    • A known person health calculation function can be changed during run time.That is, the health calculation function is no longer a member function within the GameCharacter inheritance system.
  3. Replace the virtual function with tr1::function.

  4. Replace virtual functions within the inheritance system with virtual functions within another inheritance system (policy mode).

Clause 36: Never redefine inherited non-virtual functions

  1. Because the non-virtual function is statically bound (clause 37).PB is declared as a pointer-to-B, and the non-virtual function called through pB is always the version defined by B.However, virtual functions are dynamically bound (clause 37), so virtual functions are not subject to this constraint, that is, they are called through a pointer, and the function actually called is the one that the pointer actually points to the object.

    class B{
    public:
        void mf();
        ……
    };
    
    class D: public B {……};
    
    //call
    D x;
    B* pB=&x;
    pB->mf();//Although pointing to a derived class object, it can only be the version defined by B, not a polymorphic behavior
    D* pD=&x;
    pD->mf();
    
    
  2. Remember that under no circumstances should an inherited non-virtual function be redefined.

    As already known in Clause 7, destructors within a base class should be virtual; if you violate clause 7, you also violate this clause, destructors are available for each class, even if you have not written them yourself

Clause 37: Never redefine inherited default parameters

  1. For the following inheritance

    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;//Different default parameter values, bad
        ……
    };
    class Circle: public Shape{
    public:
        virtual void draw(ShapeColor color) const;
        /*When the client calls the above function, if it is called with an object, the parameter value must be specified because the function does not inherit the default value from base under static binding.*/
        /*If using pointer or reference calls, default parameter values can be inherited from base by dynamic binding without specifying them*/
        ……
    };
    
    
  2. The static type of an object is the type that is generated when it is declared in the program. Dynamic types can show what an object will do. Dynamic types can be changed during execution, and reassignment can change dynamic types.

    Shape* ps;
    Shape* pc=new Circle;//The pc dynamic type is Circle*
    Shape* pr=new Rectangle;//The dynamic type of pr is Rectangle
    

    These pointers are of static type Shape*, and dynamic type refers to the type of object currently referred to

  3. The virtual function is dynamically bound, and which function is called to implement the code depends on the dynamic type of the object being called.

    pc->draw(Shape::Red);
    pr->draw(Shape::Red);
    
  4. If there is no parameter type

    pr->draw();//Call Rectangle::draw(Shape::Red)
    

    In the above call, the pr dynamic type is Rectangle*, so the virtual function of Rectangle is called.Rectangle:: The drawfunction defaults to GREEN, but pr is the static type Shape*, so the default parameter value for this call comes from Shape class, not Rectangle class.Half the force is applied to both functions this time.

  5. The smart way to get the virtual function to show what you want to do is to use the alternative in Clause 35.

Clause 38: moulding has-a or "from something" by composite moulding

  1. Composite (also called composite composition) is a has-a relationship, meaning "there is one".We call relationships of one type containing other types composite.

    class Address{ …… };
    class PhoneNumber{ …… };
    class Person{
    public:
        ……
    private:
        std::string name;
        Address address;
        PhoneNumber mobilePhone;
    };
    
    

    The Person object contains a string, Address, PhoneNumber object, which is compound.There are also several synonyms: layering, containment, aggregation, embedding.

  2. If there is no strict is-a relationship between the two classes (set and list), inheritance is no longer applicable, and we should choose to implement them in a composite form.

Clause 39: Wise and prudent use of private inheritance

  1. Private inheritance means that all non-private members of the parent class are private in the child class.This helps us reuse parent code and prevents exposure of parent interfaces (which can only be used internally by subclasses).
  2. But private inheritance means that it is no longer an is-a relationship, but rather a has-a relationship.We can always replace private inheritance by composite and it's easier to understand, so we should still choose composite whenever we can
  3. In the extreme case, where we have a blank parent class (no non-static variables, virtual functions, no virtual base class inheritance, but typedef, enum, static, or lang-virtual functions), private inheritance can take up less space.

Clause 40: Wise and prudent use of multiple inheritance

  1. Multiple inheritance is more complex than single inheritance, and multiple inheritance can lead to ambiguity (call class name must be explicitly stated:), as well as the need for virtual inheritance.

  2. Multiple inheritance also results in diamond inheritance

     	class File{……};
        class InputFile: public File{……};
        class OutputFile: public File{……};
        class IOFile:public InputFile, public OutputFile
        {……};
    

    IOFile has two copies of File (if File has a member variable, two copies of IOFile are duplicates)

    Solution:

    1. Default scenario (perform replication as mentioned above)

    2. Make the class (File) with this data a virtual base class

      Although duplication of member variables is avoided, the compiler pays a price, objects inherited by virtual are larger than objects inherited by non-virtual, and accessing virtual base classes member variables is slower than accessing non-virtual base classes member variables.

          class File{……};
          class InputFile: virtual public File{……};
          class OutputFile:virtual public File{……};
          class IOFile:public InputFile, public OutputFile
          {……};
      
  3. If data exists in the parent class, virtual inheritance increases costs such as size, speed, initialization (and assignment) complexity, and should be avoided as much as possible.

    Advice on virtual inheritance:

    1. When unnecessary, virtual inheritance is not used, and non-virtual inheritance is usually used.
    2. If you must use virtual inheritance, try to avoid placing data within the base so you don't have to worry about the odds of initialization and assignment on these classes
  4. One scenario where multiple inheritance applies is when public inherits an interface class and private ly inherits a class that helps implement it

        class IPerson{
        public:
            virtual ~IPerson();
            virtual std::string name() const=0;
            virtual std::string birthDate() const=0;
        };
        class DatabaseID{……};
        class PersonInfo{
        public:
            explicit PersonInfo(DatabaseID pid);
            virtual ~PersonInfo();
            virtual const char* theName() const;
            virtual const char* theBirthdayDate() const;
            ……
        private:
            virtual const char* valueDelimOpen() const;
            virtual const char* valueDelimClose() const;
            ……
        };
        class CPerson: public IPerson, private PersonInfo{
        public:
            explicit CPerson(DatabaseID pid): PersonInfo(pid){}
            virtual std::string name() const
            {
                return PersonInfo::theName();
            }
            virtual std::string birthDate()
            {
                return PersonInfo::theBirthDate();
            }
        private:
            const char* valueDelimOpen() const{return "";}
            const char* valueDelimClose() const{return "";}
        };
    

Posted by Quicksilver_0 on Mon, 29 Apr 2019 05:00:36 -0700