Effective C++ 笔记 —— Item 35: Consider alternatives to virtual functions.

The Template Method Pattern via the Non-Virtual Interface Idiom:

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.

Under the NVI idiom, it's not strictly necessary that the virtual functions be private. In some class hierarchies, derived class implementations of a virtual function are expected to invoke their base class counterparts (e.g., the example on page 120), and for such calls to be legal, the virtuals must be protected, not private. Sometimes a virtual function even has to be public (e.g., destructors in polymorphic base classes — see Item 7), but then the NVI idiom can't really be applied.

The Strategy Pattern via Function Pointers:

class GameCharacter; // forward declaration 

int defaultHealthCalc(const GameCharacter& gc); // function for the default health calculation algorithm

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 GameCharacter hierarchy, 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 ters with different health-related behavior
EvilBadGuy ebg2(loseHealthSlowly); // 
  • Health calculation functions for a particular character may be changed at runtime. For example, GameCharacter might offer a member function, setHealthCalculator, that allowed replacement of the current health calculation function.

If a character's health can be calculated based purely on information available through the character's public interface, this is not a problem, but if accurate health calculation requires non-public information, it is.

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.

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.

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

Here I've highlighted the "target signature" of this tr1::function instantiation. That target signature is "function taking a const GameCharacter& and returning an int." An object of this tr1::function type (i.e., of type HealthCalcFunc) may hold any callable entity compatible with the target signature. To be compatible means that const GameCharacter& either is or can be converted to the type of the entity's parameter, and the entity's return type either is or can be implicitly converted to int.

Compared to the last design we saw (where GameCharacter held a pointer to a function), this design is almost the same. The only difference is that GameCharacter now holds a tr1::function object — a generalized pointer to a function. This change is so small, I'd call it inconsequential, except that a consequence is that clients now have staggeringly more flexibility in specifying health calculation functions:

class GameCharacter;

int defaultHealthCalc(const GameCharacter&)
{
    return 100;
}

class GameCharacter
{
public:
    typedef std::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&)
{
    short value = 0;
    // ...
    return value;
}

struct HealthCalculator 
{ 
    int operator()(const GameCharacter&) const
    {
        int value = 0;
        // ...
        return value;
    }
}; 

class EvilBadGuy : public GameCharacter 
{ 
public:
    explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
        : GameCharacter(hcf)
    {

    }
};

class EyeCandyCharacter : public GameCharacter 
{ 
public:
    explicit EyeCandyCharacter(HealthCalcFunc hcf = defaultHealthCalc)
        : GameCharacter(hcf)
    {

    }
};

class GameLevel
{
public:
    float health(const GameCharacter&) const
    {
        float value = 0.0;
        // ...
        return value;
    }
};


int main()
{
    int value = 0;

    EvilBadGuy ebg1(calcHealth); // character using a health calculation function

    value = ebg1.healthValue();

    GameCharacter::HealthCalcFunc func = HealthCalculator();
    EyeCandyCharacter ecc1(func); // character using a health calculation function object
    value = ecc1.healthValue();

    GameLevel currentLevel;
    GameCharacter::HealthCalcFunc func_bind = std::bind(&GameLevel::health, &currentLevel, std::placeholders::_1);

    EvilBadGuy ebg2(func_bind); // character using a health calculation member function; see below for details
    value = ebg2.healthValue();

     return 0;
}

About std::function and std::bind reference to link:

https://www.jianshu.com/p/f191e88dcc80

If you're more into design patterns than C++ coolness, a more conventional approach to Strategy would be to make the health-calculation function a virtual member function of a separate health-calculation hierarchy. The resulting hierarchy design would look like this:

This just says that GameCharacter is the root of an inheritance hierarchy where EvilBadGuy and EyeCandyCharacter are derived classes; HealthCalcFunc is the root of an inheritance hierarchy with derived classes SlowHealthLoser and FastHealthLoser; and each object of type GameCharacter contains a pointer to an object from the HealthCalcFunc hierarchy.

Here's the corresponding code skeleton:

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

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

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.
原文地址:https://www.cnblogs.com/zoneofmine/p/15349734.html