zoukankan      html  css  js  c++  java
  • <Effective C++>读书摘要--Inheritance and Object-Oriented Design<一>

    1、Furthermore, I explain what the different features in C++ really mean — what you are really expressing when you use a particular construct. For example, public inheritance means "is-a," and if you try to make it mean anything else, you'll run into trouble. Similarly, a virtual function means "interface must be inherited," while a non-virtual function means "both interface and implementation must be inherited." Failing to distinguish between these meanings has caused C++ programmers considerable grief.If you understand the meanings of C++'s various features, you'll find that your outlook on OOP changes. Instead of it being an exercise in differentiating between language features, it will become a matter of determining what you want to say about your software system. And once you know what you want to say, the translation into C++ is not terribly demanding.

    <Item 32> Make sure public inheritance models "is-a."

    2、If you write that class D ("Derived") publicly inherits from class B ("Base"), you are telling C++ compilers (as well as human readers of your code) that every object of type D is also an object of type B, but not vice versa. You are saying that B represents a more general concept than D, that D represents a more specialized concept than B. You are asserting that anywhere an object of type B can be used, an object of type D can be used just as well, because every object of type D is an object of type B. On the other hand, if you need an object of type D, an object of type B will not do: every D is-a B, but not vice versa.

    3、Within the realm of C++, any function that expects an argument of type Person (or pointer-to-Person or reference-to-Person) will also take a Student object (or pointer-to-Student or reference-to-Student):

    class Person {...};
    class Student: public Person {...};
    void eat(const Person& p);            // anyone can eat
    void study(const Student& s);         // only students study
    Person p;                             // p is a Person
    Student s;                            // s is a Student
    eat(p);                               // fine, p is a Person
    eat(s);                               // fine, s is a Student,
                                          // and a Student is-a Person
    study(s);                             // fine
    study(p);                             // error! p isn't a Student

    This is true only for public inheritance. C++ will behave as I've described only if Student is publicly derived from Person. Private inheritance means something entirely different (see Item 39), and protected inheritance is something whose meaning eludes me to this day.public继承是最强的耦合,和模块化程序设计原则相悖,任何采用继承的设计都应该慎重考虑

    4、there is no one ideal design for all software. The best design depends on what the system is expected to do, both now and in the future.例如penguin直接继承bird,bird中直接实现fly,还是由FlyingBird和Penguin分别继承bird,bird中不实现fly,两种继承体系针对不同的问题域都有合理的地方,当采用"penguin直接继承bird,bird中直接实现fly"方案时,There is another school of thought on how to handle what I call the "All birds can fly, penguins are birds, penguins can't fly, uh oh" problem. That is to redefine the fly function for penguins so that it generates a runtime error:

    void error(const std::string& msg);       // defined elsewhere
    class Penguin: public Bird {
    public:
      virtual void fly() { error("Attempt to make a penguin fly!");}
      ...
    };

    It's important to recognize that this says something different from what you might think. This does not say, "Penguins can't fly(编译时)." This says, "Penguins can fly, but it's an error for them to actually try to do it(运行时)."。 Item 18 explains that good interfaces prevent invalid code from compiling, so you should prefer the design that rejects penguin flight attempts during compilation to the one that detects them only at runtime.

    5、对于Square和Rectangle类以及Sets和Lists不能使用public继承,public继承的is-a是指行为方法上的含义。The is-a relationship is not the only one that can exist between classes. Two other common inter-class relationships are "has-a" and "is-implemented-in-terms-of." These relationships are considered in Items 38 and 39. It's not uncommon for C++ designs to go awry because one of these other important relationships was incorrectly modeled as is-a, so you should make sure that you understand the differences among these relationships and that you know how each is best modeled in C++.

    6、Things to Remember

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

    <Item 33> Avoid hiding inherited names

    7、 C++'s name-hiding rules do just that: hide names. Whether the names correspond to the same or different types is immaterial. 局部作用域中的变量即使和全局变量类型不同 也一样可以隐藏全局变量。可以通过::x访问全局变量,明确使用全局变量也有助于发现bug

    8、this applies even though the functions in the base and derived classes take different parameter types, and it also applies regardless of whether the functions are virtual or non-virtual.

    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:
      virtual void mf1();
      void mf3();
      void mf4();
      ...
    };
    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

    9、The rationale behind this behavior is that it prevents you from accidentally inheriting overloads from distant base classes when you create a new derived class in a library or application framework. Unfortunately, you typically want to inherit the overloads. In fact, if you're using public inheritance and you don't inherit the overloads, you're violating the is-a relationship between base and derived classes that Item 32 explains is fundamental to public inheritance. That being the case, you'll almost always want to override C++'s default hiding of inherited names.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();
      ...
    };
    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 a using 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.

    10、It's conceivable that you sometimes won't want to inherit all the functions from your base classes. Under public inheritance, this should never be the case, because, again, it violates public inheritance's is-a relationship between base and derived classes. (That's why the using declarations above are in the public part of the derived class: names that are public in a base class should also be public in a publicly derived class.) Under private inheritance (see Item 39), however, it can make sense. For example, suppose Derived privately inherits from Base, and the only version of mf1 that Derived wants to inherit is the one taking no parameters. A using declaration won't do the trick here, because a using declaration makes all inherited functions with a given name visible in the derived class. No, this is a case for a different technique, namely, a simple 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
      ...
    };
    ...
    Derived d;
    int x;
    d.mf1();                               // fine, calls Derived::mf1  mf1是纯虚函数没有实现?怎么调用?
    d.mf1(x);                              // error! Base::mf1() is hidden

    11、Another use for inline forwarding functions is to work around ancient compilers that (incorrectly) don't support using declarations to import inherited names into the scope of a derived class.

    12、That's the whole story on inheritance and name hiding, but when inheritance is combined with templates, an entirely different form of the "inherited names are hidden" issue arises. For all the angle-bracket-demarcated details, see Item 43.

    13、Things to Remember

    • Names in derived classes hide names in base classes. Under public inheritance, this is never desirable.

    • To make hidden names visible again, employ using declarations or forwarding functions.

    <Item 34> Differentiate between inheritance of interface and inheritance of implementation

    14、The seemingly straightforward notion of (public) inheritance turns out, upon closer examination, to be composed of two separable parts: inheritance of function interfaces and inheritance of function implementations. The difference between these two kinds of inheritance corresponds exactly to the difference between function declarations and function definitions discussed in the Introduction to this book.

    15、The two most salient features of pure virtual functions are that they must 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 function interface only.

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

    class Shape {
    public:
      virtual void draw() const = 0;
      virtual void error(const std::string& msg);
      int objectID() const;
      ...
    };
    class Rectangle: public Shape { ... };
    class Ellipse: public Shape { ... };
    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

     Aside from helping you impress fellow programmers at cocktail parties, knowledge of this feature is generally of limited utility. As you'll see below, however, it can be employed as a mechanism for providing a safer-than-usual default implementation for simple (impure) virtual functions.

    16、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 function interface as well as a default implementation.

    17、It turns out that it can be dangerous to allow simple virtual functions to specify both a function interface and a default implementation.类体系中新添加的子类可能会无意中使用到基类的默认实现。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
    }
    class ModelA: public Airplane { public: virtual void fly(const Airport& destination) { defaultFly(destination); } ... }; class ModelB: public Airplane { public: virtual void fly(const Airport& destination) { defaultFly(destination); } ... };

    It's also important that Airplane::defaultFly is a non-virtual function(另一点是protected). This is because no derived class should redefine this function, a truth to which Item 36 is devoted. If defaultFly were virtual, you'd have a circular problem: what if some derived class forgets to redefine defaultFly when it's supposed to?同一个类中一个虚函数调用了另一个虚函数,设计可能存在问题

    18、Some people object to the idea of having separate functions for providing interface and default implementation, such as fly and defaultFly above. For one thing, they note, it pollutes the class namespace with a proliferation of closely related function names. Yet they still agree that interface and default implementation should be separated. How do they resolve this seeming contradiction? By taking advantage of the fact that pure virtual functions must be redeclared in concrete derived classes, but they may also have implementations of their own. Here's how the Airplane hierarchy could take advantage of the ability to define a pure virtual function: 

    class Airplane {
    public:
      virtual void fly(const Airport& destination) = 0;
      ...
    };
    void Airplane::fly(const Airport& destination)     // an implementation of
    {                                                  // a pure virtual function
      default code for flying an airplane to
      the given destination
    }
    
    class ModelA: public Airplane {
    public:
      virtual void fly(const Airport& destination)
      { Airplane::fly(destination); }
      ...
    };

     In merging fly and defaultFly, however, you've lost the ability to give the two functions different protection levels: the code that used to be protected (by being in defaultFly) is now public (because it's in fly).

    19、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.

    20、两个常见的错误:The first mistake is to declare all functions non-virtual. That leaves no room for specialization in derived classes; non-virtual destructors are particularly problematic (see Item 7). Of course, it's perfectly reasonable to design a class that is not intended to be used as a base class. In that case, a set of exclusively non-virtual member functions is appropriate. Too often, however, such classes are declared either out of ignorance of the differences between virtual and non-virtual functions or as a result of an unsubstantiated concern over the performance cost of virtual functions. The fact of the matter is that almost any class that's to be used as a base class will have virtual functions (again, see Item 7).

    If you're concerned about the cost of virtual functions, allow me to bring up the empirically-based rule of 80-20 (see also Item 30), which states that in a typical program, 80% of the runtime will be spent executing just 20% of the code. This rule is important, because it means that, on average, 80% of your function calls can be virtual without having the slightest detectable impact on your program's overall performance. Before you go gray worrying about whether you can afford the cost of a virtual function, take the simple precaution of making sure that you're focusing on the 20% of your program where the decision might really make a difference.

    The other common problem is to declare all member functions virtual. Sometimes this is the right thing to do — witness Item 31's Interface classes. However, it can also be a sign of a class designer who lacks the backbone to take a stand. Some functions should not be redefinable in derived classes, and whenever that's the case, you've got to say so by making those functions non-virtual. It serves no one to pretend that your class can be all things to all people if they'll just take the time to redefine all your functions. If you have an invariant over specialization, don't be afraid to say so!

    21、Things to Remember

    • Inheritance of interface is different from inheritance of implementation. Under public inheritance, derived classes always inherit base class interfaces.

    • Pure virtual functions specify inheritance of interface only.

    • Simple (impure) virtual functions specify inheritance of interface plus inheritance of a default implementation.

    • Non-virtual functions specify inheritance of interface plus inheritance of a mandatory implementation. 

    <Item 35> Consider alternatives to virtual functions

    22、The Template Method Pattern via the Non-Virtual Interface Idiom.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

    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
    };

    An advantage of the NVI idiom is suggested by the "do 'before' stuff" and "do 'after' stuff" comments in the code. Those comments identify code segments guaranteed to be called before and after the virtual function that does the real work. This means that the wrapper ensures that before a virtual function is called, the proper context is set up, and after the call is over, the context is cleaned up. For example, the "before" stuff could include locking a mutex, making a log entry, verifying that class invariants and function preconditions are satisfied, etc. The "after" stuff could include unlocking a mutex, verifying function postconditions, reverifying class invariants, etc. There's not really any good way to do that if you let clients call virtual functions directly.子类需要覆写自己本身不调用的private virtual函数是正常的。NVI下对应的虚函数也可以是protected的,甚至为public,但是此时就不能实施NVI了。私有化这个虚函数,有助于设计上的层次更分明,应该避免同一个层次间的函数调用,或是让模块对外暴露不同的层次。具体的说doHealthValue被healthValue调用,他们就应该处于不同的层次上(doHealthValue层次更低一些),同时对外暴露这两个接口也是错误的。

    23、The Strategy Pattern via Function Pointers 

    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;
    };

     这种方案相比虚函数方案有两个优点。1、Different instances of the same character type can have different health calculation functions.2、Health calculation functions for a particular character may be changed at runtime.

    24、使用类外的非友元函数进行计算的时候,存在一个访问计算所需数据的问题。As a general rule, the only way to resolve the need for non-member functions to have access to non-public parts of a class is to weaken the class's encapsulation. For example, the class might declare the non-member functions to be friends, or it might offer public accessor functions for parts of its implementation it would otherwise prefer to keep hidden. Whether the advantages of using a function pointer instead of a virtual function (e.g., the ability to have per-object health calculation functions and the ability to change such functions at runtime) offset the possible need to decrease GameCharacter's encapsulation is something you must decide on a design-by-design basis.

    25、The Strategy Pattern via tr1::function.

    Once you accustom yourself to templates and their use of implicit interfaces (see Item 41), the function-pointer-based approach looks rather rigid. Why must the health calculator be a function instead of simply something that acts like a function (e.g., a function object)? If it must be a function, why can't it be a member function? And why must it return an int instead of any type convertible to an int?

    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;
    };
    class EvilBadGuy: public GameCharacter {         // as before
      ...
    };
    GameLevel currentLevel;
    ...
    EvilBadGuy ebg2(                                   // character using a
      std::tr1::bind(&GameLevel::health,               // health calculation
              currentLevel,                            // member function;
              _1)                                      // see below for details
    );

    相比函数指针,tr1::function更灵活。by using tr1::function instead of a function pointer, we're allowing clients to use any compatible callable entity when calculating a character's health. Is that cool or what?

    26、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;
    };

    一方面可以让熟悉设计模式的快速看懂代码,另外一方面方便新添加health的计算方法

    27、The fundamental advice of this Item is to consider alternatives to virtual functions when searching for a design for the problem you're trying to solve. Here's a quick recap the alternatives we examined:

    • 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.

    28、Things to Remember

    • Alternatives to virtual functions include the NVI idiom and various forms of the Strategy design pattern. The NVI idiom is itself an example of the Template Method design pattern.

    • A disadvantage of moving functionality from a member function to a function outside the class is that the non-member function lacks access to the class's non-public members.

    • tr1::function objects act like generalized function pointers. Such objects support all callable entities compatible with a given target signature.

  • 相关阅读:
    Prometheus监控k8s集合
    docker集合
    开源堡垒机jumpserver
    ELK日志分析平台
    安全名称解释
    CPU上下文切换
    平均负载
    234. 回文链表
    125. 验证回文串
    122. 买卖股票的最佳时机II
  • 原文地址:https://www.cnblogs.com/lshs/p/4575551.html
Copyright © 2011-2022 走看看