看了《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