看了《More Effective C++》、《Modern C++ Design》等书,总觉得应该上手练习一下……于是我想到了“属性”。我希望我所完成的属性具有以下特性:
1、语法上与C#、ActionScript保持一致,即:Obj.Property = propValue和PropValueVar = Obj.Property;
2、根据被使用的context(l-value与r-value)使用预先设定的getter和setter函数;
3、既然属性中只是调用了getter和setter函数,那么应该不占用内存;
4、声明属性应尽量简单,类似这样的语法:property int propValue(get = &Class::Getter, set = &Class::Setter);
5、有readonly属性、static属性、static readonly属性等……
看来我应该使用ProxyClass(以完成某项功能为唯一目标的“代理”类,见《More Effective C++》条款30)。这里,使用ProxyClass来区分l-value与r-value是再标准不过的用例。
Level 1
第一份实现:
template <class PropType>
class Property
{
private:
Property() {}
~Property() {}
public:
operator PropType() //used as r-value
{
//call host class’s getter function
return getter();
}
Property& operator=(PropType rhs) //used as l-value
{
//call host class’s setter function
setter(rhs);
return *this;
}
};
operator PropType()可以自动将Property隐式转换成PropType,即用为r-value的情况;operator=()在Property被赋值时调用,即用为l-value的情况。有了这2个运算符重载,已经可以实现目标1,对于目标2也给出了足够的空间。
Level 2
但问题马上就出现了:上述代码中的加粗行,只是伪代码而已;如何真正实现对getter与setter的调用?
最显然的方案就是在Property中保存HostClass(持有此Property声明的类)对象的指针与getter和setter函数的指针(成员函数指针),然后调用。但就算是这样显然的实现,也有个问题:怎么构造Property。现在Property必须在构造函数中传入这3个指针,这就导致使用Property的类的构造函数中必须加入构造Property的相关代码(此问题可以使用std::mem_fun_t来解决,其实这正是我的第一个实作版本)……除去这些不算,现在Property类开始占用内存了。想象一下当一个类中有大量的Property,每个Property保存有关于HostObject的3个指针……
我们只是需要简单的将属性重定向到一个getter函数与一个setter函数而已。为了充分压榨Compiler的劳动力,想想在目前的情况下哪些东西可以在编译期确定。2个成员函数指针显然可以。但我们仍旧需要HostObject的指针。好吧,我承认当初我自己也没有搞定这个问题,直到我读了《Imperfect C++》第35章(本文的参考)。我们有Property类的this指针,也知道Property对象作为HostObject对象的一部分。它们各自的this指针之间的偏移量正是那第三个“编译期常数”。位于stddef.h中的offsetof宏正可以帮助我们计算类与成员间的偏移量。这样,通过Property对象内部的this指针加3个编译期常数,可以推导出HostClass*、&HostClass::getter、&HostClass::setter。
Level 3
下一步就是将这3个常数作为模板常数参数来生成我们的Property类。新的问题再次产生,Property模板参数必须要在HostClass声明中声明;而偏移量此时并不能计算,因为HostClass还未完成声明……无论如何,你都会得到一个HostClass undefined的编译错误。《Imperfect C++》再次帮助了我们:将计算偏移量放入一个静态成员函数中,并将该函数作为模板参数代替偏移量本身,只要你在声明Property前声明了这个静态成员函数就可以做到。
看看我们目前为止完成了哪些工作:
template <class PropType,
class PropHost,
PropType (PropHost::*GetterFn)() const,
void (PropHost::*SetterFn)(PropType),
int (*PropOffset)()>
class Property
{
private:
friend PropHost;
Property() {};
public:
Property& operator=(PropType rhs)
{
// offset this pointer to host class pointer
// and call member function
((PropHost*)((unsigned char*)this - (*PropOffset)())
->*SetterFn)(rhs);
return *this;
}
operator PropType() const
{
return ((PropHost*)((unsigned char*)this - (*PropOffset)())
->*GetterFn)();
}
};
很不错吧?能够完成功能,并且不占用内存。更重要的是,我们可以指望这些代码都被内联进使用属性的地方,从而将额外开销降低为0。将HostClass声明为友元可以防止Property在类外被构造(如果你硬要在类的成员函数中构造也没办法……)。相比第一份实现,我去掉了private的析构函数声明,理由是既然Property不能被构造,那么也没必要刻意去防止被析构。Property被析构的唯一情况是delete &TestObj.TestProperty,不过同样的语法也可以被应用在内建型别上(即:delete &TestObj.intMember),因此没有必要将阻止写出这种垃圾代码的责任揽在自己头上。
Level 4
让我们来看看怎么使用这个类来声明属性:
class TestClass
{
private:
int getIntValue() const
{
return m_Value;
}
void setIntValue(int v)
{
m_Value = v;
}
int m_Value;
static int Offset()
{
return offsetof(TestClass, intValue);
}
public:
Property<int,
TestClass,
&TestClass::getIntValue,
&TestClass::setIntValue,
&TestClass::Offset,
> intValue;
};
TestClass t;
t.intValue = 5;
cout << t.intValue << endl; //5
一切正常。
还记得本文开头的设计目标吗?我们已经完成了1、2、3(3还不完备,详见下),但4:简洁的声明语法显然还未实现。
宏可以很好的帮助我们完成这个目标:
#define DECLARE_PROPERTY(PropHost, Modifier, PropType, PropName, Getter, Setter)"
private: static int __##PropName##_Offset() { "
return offsetof(PropHost, PropName); } "
Modifier: Property<PropType, "
PropHost, "
&PropHost::__##PropName##_Offset, "
&PropHost::Setter, "
&PropHost::Getter, "
> PropName
参数Modifier为访问修饰符,其他的意义都很明了。有了这个宏,在类中声明属性就变得很简单:
DECLARE_PROPERTY(TestClass, public, int, intValue, getIntValue, setIntValue);
如你所见,这样的声明还是显得冗长。我们希望可以不用写无数遍HostClass名字,不用加一个别扭的public,最好是这样:
public:
DECLARE_PROPERTY(int, intValue, getIntValue, setIntValue);
能做到吗?能,但是还有一个问题需要说明。
Level 5
C++中不允许长度为0的struct/union/class真正存在。不信的话可以测试如下代码:
struct empty {};
cout << sizeof(empty) << endl; // 1
因此虽然我们的Property中不含任何状态,但长度始终不会是0。如果一个类中声明大量的属性,则它们导致的类长度增加就很客观了。我们可以使用union结构来将此影响降低到最小。
我想你很快就能想到匿名union结构,既可以应用到union带来的内存奖励又可以不用写额外的成员访问。更妙的是,通过在这个匿名union中声明一个“private”的typedef TestClass this_class,可以做到在属性声明中不重复写HostClass……
可惜的是,这样的结构并不可行:关键就在于匿名union结构不允许有protected和private成员。关于typedef的美梦破灭了,另一个本来做得好好的美梦也破灭了:在我们的DECLARE_PROPERTY中有私有的静态成员函数用来计算属性的偏移值,现在除非把它改成public,否则别想过Compiler这一关。《Imperfect C++》中的做法是将计算偏移量的函数声明放在另外一个宏中,这样需要用户自己来使用这个宏——感觉离目标4愈发遥远了。
让我们稍许妥协一下。你觉得t.Properties.intValue相比t.intValue来说怎么样?如果还能忍受就往下看吧。
我们使用具名union来代替匿名union。好处有很多,最重要的是它可以拥有private成员。因此之前关于typedef的美梦也可以继续了。而且,使用union来组织属性,这些属性在类中的偏移就都一样了,可以只写出一份Offset函数。另外,使用t.Properties.intValue这样的属性访问语法,可以提醒用户这只是个属性而不是真正的数据成员,以减少他们将属性用作函数的reference参数与pointer参数。
#define BEGIN_PROPERTIES_WITHNAME(HostClass, Name) "
union __Properties "
{ "
private: "
typedef HostClass __HostClass; "
static int __Offset() { "
return offsetof(__HostClass, Name); } "
public:
#define END_PROPERTIES_WITHNAME(Name) } Name
#define BEGIN_PROPERTIES(HostClass) "
BEGIN_PROPERTIES_WITHNAME(HostClass, Properties)
#define END_PROPERTIES() "
END_PROPERTIES_WITHNAME(Properties)
#define PROPERTY_READWRITE(PropType, PropName, Getter, Setter) "
struct { Property<PropType, "
__HostClass, "
&__HostClass::Getter, "
&__HostClass::Setter, "
&__HostClass::__Properties::__Offset "
> PropName; };由于持有属性的HostClass从PropHost变为了PropHost::__Properties,因此Property中的友元声明也需要作相应修改。
请注意我在PROPERTY_READWRITE中使用了匿名struct结构来包覆Property。原因是C++对于union的另一个限制:union的成员不能有默认的构造函数或其他不常用的构造函数。大致的理由我猜是因为union根本不知道要构造哪个成员而不能保证成员的默认构造函数被正确调用吧。加上这条可以绕过这个限制;我觉得这大概可以算是个bug(在Visual Studio 2008中),不过好在我们的Property不需要构造函数。
现在,在类中声明属性变得很直观:
class TestClassA
{
public:
explicit TestClassA(int v) : m_Value(v) {}
private:
int getIntValue() const
{
std::cout << "TestClassA::getIntValue()" << std::endl;
return m_Value;
}
void setIntValue(int value)
{
std::cout << "TestClassA::setIntValue()" << std::endl;
m_Value = value;
}
int m_Value;
public:
BEGIN_PROPERTIES(TestClassA)
PROPERTY_READWRITE(int, intValue, getIntValue, setIntValue)
END_PROPERTIES();
};
Level 6
前4项设计目标基本都比较好地实现了。剩下最后一条,我想说:让我们先暂时不讨论static吧。
只读属性的实现和已完成的Property相比,没有太大区别,我把我自己实作过程中所遇见的2个问题列出:
1. 我最原始的想法是使用模板偏特化,在Property<PropType, PropHost, GetterFn, SetterFn, OffsetFn>的基础上做出typedef Property<PropType, PropHost, GetterFn, NULL, OffsetFn> ReadonlyProperty。遗憾的是,C++再次阻止了我这一企图,原因在于偏特化的常数参数类型不能有依赖性。我们的Setter的类型void (PropHost::*SetterFn)(PropType)刚好依赖于PropHost与PropType……于是只能重写一个新的ReadonlyProperty,与Property相比去掉了operator=以实现只读访问;
2. C++的默认operator=机制允许ReadonlyProperty如此使用:
t.Properties.readonlyValue = t.Properties.readonlyValue;
我们当然不能允许此种情况发生。因此,可以显式在ReadonlyProperty声明中进行禁止:
private:
ReadonlyProperty& operator=(const ReadonlyProperty&);
可以不用定义(要定义就是{}):因为从来没有被使用过(已经被禁止掉了)。问题是将ReadonlyProperty放在union中。C++的union不但不允许默认构造函数,连此类的operator=同样也不行。不过我们通过匿名struct已经绕过这个问题。
Level 7
还有2个问题。
属性的类型如果不是内建型别,而是struct,pointer,抑或是class怎么办?
解决方法是不要解决。没有什么可担心的,直接将reference type或pointer type传给Property模板。具体的行为不在于Property,而在于你使用的Getter和Setter函数。不过,这样使用起来有点不方便,具体来说是const修饰符作用在Getter与Setter函数的签名上。可以写相应的const版本的Property类来满足需求。
另外一点。看下面的代码:
// inside a class declaration...
private:
struct Point
{
int x;
int y;
};
public:
BEGIN_PROPERTIES(Line)
PROPERTY_READWRITE(Point&, Start, getStart, setStart)
PROPERTY_READWRITE(Point&, End, getEnd, setEnd)
END_PROPERTIES();
//continue class declaration...
那么,如何取用Point::x或Point::y就成了一个问题。我们只能这样写:
int startX = static_cast<Point&>(topLine.Properties.Start).x;
无论如何这都令人厌烦。
可以在Property中添加operator*,让其返回这个属性的真正类型:毕竟operator*的意思就是取值。
int startX = (*topLine.Properties.Start).x;
另外,operator*对于内建型别也是有意义的。看如下代码:
float getAvg(float x1, float x2)
{
return (x1 + x2) * 0.5f;
}
float avg = getAvg(*t.Properties.intValue, *u.Properties.intValue);
如果没有operator*,那么这样的隐式转换(int->float)是不可能成功的。
原文地址: http://xiaolingyao.spaces.live.com/blog/cns!6A8F02D95D2DDE46!201.entry