C++设计模式(避免virtual函数)
本文讨论C++中一些特别的设计模式来避免使用virtual函数。(因为调用virtual函数的开销比较大。)
假设我们现在要设计一个游戏,为这个游戏内的人物设计一个继承体系。
class GameCharacter {
public:
virtual int healthValue() const; // 会有一个算法来计算。
...
};
这是一个非常显而易见的设计,但接下来,将介绍几种设计模式来完善这个类。
Template Method设计模式
这个模式主张virtual函数总是priavte。
class GameCharacter {
public:
int healthValue() const {
...
int retVal = doHealthValue();
...
return retVal;
}
...
private:
virtual int doHealthValue() const {
...
}
};
这个设计令客户通过public non-virtual成员函数间接调用private virtual函数,称为“non-virtual interface(NVI)手法”。他是所谓template method设计模式的一个独特表现方式。
NVI手法的一个优点是可以在虚函数调用前和调用后,附加一些相关额操作,确保虚函数进行真正工作之前和之后被调用。相当于是设定好一个场景让虚函数起作用。
并且,NVI手法允许derived class重新定义virtual函数,从而赋予他们“如何实现机能”的控制能力,但base class保留着“函数何时被调用”的权利。
Strategy设计模式-Function Pointer
此方法让函数指针成为成员,从而能够改变健康值的计算算法。
class GameCharacter; // forward declaration;
// default 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);
}
void setFunc(HealthCalcFunc anoFunc) {
healthFunc = anoFunc;
{
...
private:
HealthCalcFunc healthFunc;
};
这个设计方式可以给healthValue的算法提供弹性,从而让不同的对象可以有不同的计算方法,甚至在不同的时期可以有不同的计算方法。
但这种设计模式也有可能出现问题,例如如果计算方法不能仅通过public接口提供的信息计算出来,解决这个问题的方法就是“弱化class的封装”,通过给出特定信息的访问函数,或者设定friend函数。
运用函数指针来替换virtual函数,其优点就在于,每个对象都可以有个字的计算函数,并且在运行期改变计算函数,但伴随着类的封装性降低。是否值得,得根据具体情况而定。
附言:typedef用法
在编程中使用typedef目的一般有两个,一个是给变量一个易记且意义明确的新名字,另一个是简化一些比较复杂的类型声明。
使用方法: typedef existing_type new_type_name ;
最简单使用:
typedef int size; // 把size当做int使用
typedef unsigned int WORD;
数组和指针
typedef char Line[81];
Line text, secondline; // 把Line定义为char[81]
typedef char* pstr; // 把pstr定义为char*
pstr str = "abc";
int mystrcmp(pstr, pstr);
函数
void printHello(int i);
typedef void (*pFunc)(int); // 把pFunc定义为参数为int,返回类型为void的函数指针。
pFunc temp;
temp = printHello;
temp(110);
Strategy设计模式–function对象
使用函数指针显得太过拘束。我们总是希望有更大的操作弹性,例如可以使用函数对象、成员函数指针,某些安全的隐式类型转换。所以我们优化函数指针而使用function容器。
首先我们先来介绍一下function和bind。
function
在C++中,可调用实体主要包括函数,函数指针,函数引用,可以隐式转换为函数指定的对象,或者实现了opetator()的对象(即C++98中的functor)。C++0x中,新增加了一个std::function对象,std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(我们知道像函数指针这类可调用实体,是类型不安全的)。
包装普通函数
int Add(int x, int y) {
return x + y;
}
function<int (int, int) > f = Add; // 第一个参数是返回类型,省下的都是参数类型。
int z = f(2, 3);
包装函数对象
class CxxPrint {
public:
size_t operator()(const char*) { ... }
};
CxxPrint CPrint;
function< size_t (const char*) > f;
f = CPrint;
f(char ...);
包装类的成员函数
#include <functional>
class TAdd {
public:
int Add(int x, int y) {
return x + y;
}
};
int main(void) {
TAdd tAdd;
function<int (TAdd*, int, int) > f = &TAdd::Add;
cout << f(&tAdd, 2, 3);
return 0 ;
}
保存函数对象的状态
#include <iostream>
#include <functional>
using namespace std;
class TAdd {
public:
TAdd() : val(0) {
}
int operator() (int i) {
val += i;
return val;
}
int Sum() const {
return val;
}
private:
int val;
};
int main(void) {
TAdd tAdd;
function<int (int) > f1 = tAdd;
function<int (int) > f2 = tAdd;
cout << f1(10) << "," << f2(10) << "," << tAdd.Sum() << endl;
function<int (int) > f1_ref = ref(tAdd);
function<int (int) > f2_ref = ref(tAdd);
cout << f1(10) << "," << f2(10) << "," << tAdd.Sum() << endl;
return 0 ;
}
/* output:
10,10,0
20,20,0
Program ended with exit code: 0
*/
function<int (int) > f1 = tAdd;
此函数对象实际上是拷贝给了f1,所以tAdd和f1是两个不同的对象。所以他们之间并不会相关关联,所以一个输出结果会是三个不同对象的输出。同时C++也提供了ref和cref函数,来提供对象的引用和常引用的包装。于是就可以正确的保存函数对象的状态了。
bind
// bind example
#include <iostream> // std::cout
#include <functional> // std::bind
// a function: (also works with function object: std::divides<double> my_divide;)
double my_divide (double x, double y) {return x/y;}
struct MyPair {
double a,b;
double multiply() {return a*b;}
};
int main () {
using namespace std::placeholders; // adds visibility of _1, _2, _3,...
// binding functions:
auto fn_five = std::bind (my_divide,10,2); // returns 10/2
std::cout << fn_five() << '\n'; // 5
auto fn_half = std::bind (my_divide,_1,2); // returns x/2
std::cout << fn_half(10) << '\n'; // 5
auto fn_invert = std::bind (my_divide,_2,_1); // returns y/x
std::cout << fn_invert(10,2) << '\n'; // 0.2
auto fn_rounding = std::bind<int> (my_divide,_1,_2); // returns int(x/y)
std::cout << fn_rounding(10,3) << '\n'; // 3
MyPair ten_two {10,2};
// binding members:
auto bound_member_fn = std::bind (&MyPair::multiply,_1); // returns x.multiply()
std::cout << bound_member_fn(ten_two) << '\n'; // 20
auto bound_member_data = std::bind (&MyPair::a,ten_two); // returns ten_two.a
std::cout << bound_member_data() << '\n'; // 10
return 0;
}
(From cplusplus.com)
auto是一种自动类型,会在运行的时候自动转化为相应的类型。
优化
class GameCharacter; // forward declaration;
// default algorithm
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
typedef std::function<int (const GameCharacter&) > HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {
}
int healthValue() const {
return healthFunc(*this);
}
void setFunc(HealthCalcFunc anoFunc) {
healthFunc = anoFunc;
{
...
private:
HealthCalcFunc healthFunc;
};
function类型产生的对象可以持有任何与此签名式兼容的可调用物。所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可以被隐式转换为int。
这个设计模式使得“指定函数算法”这件事上有惊人的弹性:
short calcHealth(const GameCharacter&);
struct HealthCalculator {
// 函数对象
int operator()(const GameCharacter&) const {
...
}
};
class GameLevel {
public:
float health(const GameCharacter&) const;
...
};
class EvilBadGuy : public GameCharacter {
...
};
EvilBadGuy ebg1(calcHealth); // 使用函数
EvilBadGuy ebg2(HealthCalculator()); // 使用函数对象
GameLevel currentLevel;
...
EvilBadGuy edg3(
std::bind(&GameLevel::health, currentLevel, _1)); // 使用成员函数。
// 实际上就是调用currentLevel对象里的health函数作为edg3成员。