Article 35: Consider alternatives other than the virtual function
- Article 35 consider alternatives other than the virtual function
- Realizing template method mode by Non-virtual interface technique
- Implementing strategy mode with function pointers
- Complete strategy mode with Tr1function
- The classical strategy model
- Summary
Virtual functions are often used in derivation, and it is no problem to use virtual functions when encountering some problems, but sometimes we should consider whether there are alternatives to broaden our horizons.
If you are writing a game now, the amount of blood in the game decreases with the battle, and a function is used to healthValue
return the amount of blood. Because different people's blood measurement method is different, so we should say Healthvalue declaration for virtual:
class GameCharacter{ public: virtualinthealthValueconst;//derived classes可以重新定义 …… };
healthValue
Not pure virtual, which implies that we have a default algorithm for calculating the amount of blood. (* * clause **34)
This is a very clear design and it is precisely because of this that we may not consider alternative alternatives. I'm out of the ordinary, let's consider some other solution.
Realizing template method mode by Non-virtual interface technique
Let's look at one proposition: the virtual function should be almost always private. This assertion suggests that a better design is to keep the healthvalue as public non-virtual member function, and let it invoke a private virtual function to do the actual work:
class gamecharacter{public : int healthvalue () const {... //do the work beforehand int retval=dohealthvalue (); //really do the actual work ... //working after work return retVal; } ...... private : virtual int dohealthvalue () const //derived Classes can redefine {...} };
This design is to let the client indirectly call the private virtual function through public non-virtual member function, become non-virtual interface(NVI) method. It is a unique manifestation of the so-called Template Method design pattern (which does not matter with C + + templates). This non-virtual function is called the virtual function's outer cover (wrapper).
NVi's advantages lie in "doing the work beforehand" and "working after work", which ensures that the virtual function does some work before and after the call, prepares for the virtual function transfer, and cleans up after the call. For example, pre-work can include locking mutexes, manufacturing logging entries, validating class constraints, verifying function prerequisites, and so on, after which the work can include unblocking the mutex, validating the function after the condition, and so on.
It is necessary to explain how to redefine the virtual function and call the virtual function. Redefining the virtual function means that something is done, and calling the virtual function indicates when to do something. This is not contradictory. Derived classes redefine the virtual functions to give them control over how they are implemented, but the base class retains the right to invoke the function.
NVi technique may not necessarily let the virtual function is private, some classes inheritance system requirements derived class is protected. However, if the virtual function is public (for example, the destructor of base classes, * * clause **7), then the NVi method cannot be implemented.
By the function pointers implementation
StrategyMode
NVI uses the virtual function to calculate the health index of each person, and there is a claim that the figure Health index is not related to the character type, so the calculation does not require the "people" component. For example, you can accept a pointer in the character's constructor, pointing to the health calculation function:
classGamecharacter;//forward Declaration intDefaulthealthcalc (Constgamecharacter& GC);//Health calculation default algorithm classgamechaaracter{ Public:typedef int(*healthcalcfunc) (Constgamecharacter&);ExplicitGamecharacter (Healthcalcfunc Hcf=defaulthealthcalc): Healthfunc (HCF) {}intHealthvalue ()Const{returnHealthfunc (* This); } ......Private: Healthcalcfunc Healthfunc;//function pointer};
In fact, this is the application of strategy design pattern. More elasticity compared to using the virtual function
- Different entities of the same character type can have different health calculation functions. This means that different objects of the same person type can have different health calculations, such as shooting games, some people who buy bulletproof vests use objects, the amount of blood can be reduced more slowly.
- A known figure 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.
In fact, it is controversial that a function within class (perhaps a member function) is replaced with an external equivalent function of class (for example, a non-member, non-friend function, or a non-friend member function of another class).
When accessing internal members by an external function, it is sometimes necessary to weaken the class's encapsulation. This also brings a disadvantage, but the merits (above 2 points) are sufficient to compensate for the shortcomings, depending on the situation.
Complete with tr1::function
StrategyMode
If you are accustomed to templates and their use of the implicit interface (* * clause **41), the implementation based on function pointers is a bit inflexible.
Instead of using a function pointer, you can use an object of type Tr1::function. Such objects can have (save) any callable object (callable entity, function pointer, function object, member function pointer) as long as its signature is compatible with the demand side. Implemented with Tr1::function
classGamecharacter;//forward Declaration intDefaulthealthcalc (Constgamecharacter& GC);classgamecharacter{ Public:typedef STD::tr1::function<int(Constgamecharacter&) > Healthcalcfunc;ExplicitGamecharacter (Healthcalfunc Hcf=defaulthealthcalc): Healthfunc (HCF) {}intHealthvalue ()Const{returnHealthfunc (* This); } ......Private: Healthcalcfunc Healthfunc; };
In the above procedure, HealthCalcFunc
typedef std::tr1::function<int (const GameCharacter&)>
where the content in the angle brackets is the target signature of the tr1::function (instantiation), the function that the signature represents is " Accept a reference point to const gamecharacter and return int ". Objects produced by the tr1::function type (that is, the Healthcalcfunc type) can hold (save) any callable object that is compatible with this signature (callable entity). Compatibility means that the parameters of this callable object can be implicitly converted to const Gamecharacter&, and its return type can be implicitly converted to int.
Compared to a function pointer, this design simply turns the function pointer into a Tr1::function object (equivalent to a generalized pointer). This change is small, but provides greater elasticity. For example
ShortCalchealth (Constgamecharacter&);//Health calculation function, return type is not int structhealthcalculator{//Function object designed for health computing int operator()(Constgamecharacter&)Const{......} }; Class gamelevel{ Public:float Health(Constgamecharacter&)Const;//member function calculates health, returns not int...... }; Class Evilbadguy: Publicgamecharacter{//Same as before...... }; Class Eyecandycharacter: Publicgamecharacter{//Another character, assuming its constructors and Evilbadguy are the same...... };//People 1, using a function to calculate health indexEvilbadguy EDG1 (calchealth);//People 2, using function object to calculate health indexEyecandycharacter ecc1 (Healthcalculator ()); Gamelevel CurrentLevel;//People 3, using a member function to calculate health indexEvilbadguy ebg2 (Std::tr1::bind (&gamelevel::health,currentlevel,_1));
As we can see from the above example, tr1::function
the elasticity is far greater than the function pointer. Let tr1::bind
's take a look at what happened.
When calculating the health index of EBG2, the member function of Gamelevel class is used. GameLevel::health
A parameter is accepted on the surface, but it actually accepts two parameters, and it also accepts a currentlevel with the parameter type Gamelevel, which is the this pointer. The Gamecharacter health calculation function accepts only one parameter gamecharacter, but Gamelevel::health accepts two parameters, so it is used tr1::bind
. "_1" indicates that Ebg2 calls Gamelevel::health with CurrentLevel as the Gamelevel object.
As can be seen from the above example, the tr1:;function
replacement function pointer will allow the client to use any compatible callable object (callable entity) when calculating the character health index.
of classical
StrategyMode
In the above UML diagram, Gamecharacter is the base class in an inheritance system, and Evilbadguy and Eyecandycharacter are derived classes. Healthcalcfunc is the base class of another inheritance system, Slowhealthloser and Fasthealthloser are derived classes, and each Gamecharacter object has pointers, Point to an object from the Healthcalcfunc inheritance system.
Class Gamecharacter;//forward DeclarationClass healthcalcfunc{ Public: ......Virtual int Calc(Constgamecharacter& GC)Const{ ...... } ...... }; Healthcalcfunc Defaulthealthcalc; Class gamecharacter{ Public:Explicit Gamecharacter(healthcalcfunc* Phcf=&defaulthealthcalc):p Healthcalc (PHCF) {}intHealthvalue ()Const{returnPhealthclac->calc (* This); } ......Private: healthcalcfunc* Phealthcalc; };
People familiar with the standard strategy model can easily identify it, and it also provides the possibility of "incorporating an existing health computing approach" as long as a derived class is added to the Healthcalcfunc inheritance system.
Summary
This article mainly describes, for the virtual function to find alternative solutions, there are several alternatives:
- Using the Non-virtual interface (NVI) technique, this is a special form of the Template method design pattern. It takes the public non-virtual member function to wrap the virtual function with lower accessibility (private or protected).
- Replace the virtual function with a function pointer member variable, which is a decomposition representation of the strategy design pattern.
- Replace the virtual function with the Tr1::function member, which allows any callable object (callable entity) to be paired with a demand-compatible signature. This is also some form of strategy design pattern.
- Replace the virtual function in the inheritance system with another virtual function of the inheritance system. This is the traditional expression of strategy design pattern.
These are just a few alternatives to the virtual function, not all of them. You should also consider the pros and cons of each scenario.
Summary
-The alternative to the virtual function includes many forms of nvi manipulation and strategy design patterns. The NVI technique itself is a special form of Template Method design mode.
-The disadvantage of moving functions from member functions to class external functions is that non-member functions cannot access the Non-public members of class.
-The Tr1::function object behaves like a generic function pointer. Such objects have greater elasticity.
"Effective C + +": Clause 35: Consider alternatives other than the virtual function