1. 所谓代理类(proxy class),指的是"它的每一个对象都是为了其他对象而存在的,就像是其他对象的代理人一般".某些情况下用代理类取代某些内置类型可以实现独特的功能,因为可以为代理类定义成员函数而但却无法对内置类型定义操作.条款5就展示了一个使用代理类阻止隐式类型转换的例子.
2. 实现二维数组.
C++没有提供分配动态二维数组的语法,因此常常需要定义一些类(模板实现这些功能),像这样:
template<class T> class Array2D { public: Array2D(int dim1, int dim2); ... };
既然是二维数组,那么有必要提供使用"[][]"访问元素的操作,然而[][]并不是一个操作符,C++也就不允许重载一个operator[][],解决办法就是采用代理类,像这样:
template<class T> class Array2D { public: //代理类 class Array1D { public: T& operator[](int index); const T& operator[](int index) const; ... }; Array1D operator[](int index); const Array1D operator[](int index) const; ... };
那么以下操作:
Array2D<float> data(10, 20); ... cout << data[3][6];
data[3][6]实际上进行了两次函数调用:第一次调用Array2D的operator[],返回Array1D对象,第二次调用Array1D的operator[],返回指定元素.
(本例对proxy class作用的体现其实并不显著,因为可以对指针施加operator[],因此只要使Array2D的oeprator[]返回一个指针可达到同样效果,没有必要使用代理类)
3. 区分operator[]的读写动作
条款29用String类的例子讨论了引用计数,由于当时无法判断non-const版本oeprator[]返回的字符将被用于读操作还是写操作,因此保险起见,一旦调用non-const版本operator[],便开辟一块新内存并复制数据结构到新内存.在这种策略下,因此如果operator[]返回的字符被用于读操作,那么分配新内存并复制数据结构的行为其实是不必要的,由此会带来效率损失,使用proxy class便可以做到区分non-const operator[]用于读还是写操作,像这样:
class String { public: //代理类用于区分operator[]的读写操作 class CharProxy { // proxies for string chars public: CharProxy(String& str, int index); // creation CharProxy& operator=(const CharProxy& rhs); // lvalue CharProxy& operator=(char c); // uses operator char() const; private: String& theString; //用于操作String,并在适当时机开辟新内存并复制 int charIndex; }; const CharProxy operator[](int index) const; // for const Strings CharProxy operator[](int index); // for non-const Strings ... friend class CharProxy; private: RCPtr<StringValue> value;//见条款29 };
对String调用operator[]将返回CharProxy对象,CharProxy通过重载oeprator char模拟char类型的行为,但它比char类型更有优势——可以为CharProxy定义新的操作,这样当对CharProxy使用operator=时,便可以得知对CharProxy进行写操作,由于CHarProxy保存了父对象String的一个引用,便可以在现在执行开辟内存并复制数据结构的行为,像这样:
String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs) { if (theString.value->isShared()) { theString.value = new StringValue(theString.value->data); } theString.value->data[charIndex] = rhs.theString.value->data[rhs.charIndex]; return *this; } String::CharProxy& String::CharProxy::operator=(char c) { if (theString.value->isShared()) { theString.value = new StringValue(theString.value->data); } theString.value->data[charIndex] = c; return *this; } //以上来那个函数的代码部分有重复,可考虑将重复部分提取成一个函数
由于内存开辟和数据结构赋值任务交由CharProxy完成,String的operator[]相当简单,像这样:
const String::CharProxy String::operator[](int index) const { return CharProxy(const_cast<String&>(*this), index); } String::CharProxy String::operator[](int index) { return CharProxy(*this, index); }
CharProxy实现的其他部分如下:
String::CharProxy::CharProxy(String& str, int index): theString(str), charIndex(index) {} String::CharProxy::operator char() const { return theString.value->data[charIndex]; }
4. 局限性.
就像智能指针永远无法完全取代内置指针一样,proxy class也永远无法模仿内置类型的所有特点.proxy class可以实现内置类型无法做到功能,但有利有弊——为了模仿内置类型的其他特点,它还要打许多"补丁".
1). 对proxy class取址.
条款29通过为StringValue类添加可共享标志(flag)来表示对象是否可被共享以防止外部指针的篡改,其中涉及到对operator[]返回值进行取址操作,这就提示CharProxy也需要对operator&进行重载,像这样:
class String { public: class CharProxy { public: ... char * operator&(); const char * operator&() const; ... }; ... };
const版本operator&实现比较容易:
const char * String::CharProxy::operator&() const { return &(theString.value->data[charIndex]); }
non-const版本的operator&要做的事情多一些:
char * String::CharProxy::operator&() { //如果正在使用共享内存,就开辟新内存并复制数据结构 if (theString.value->isShared()) { theString.value = new StringValue(theString.value->data); } //由于有外部指针指向它,因此有被篡改风险,禁止使用共享内存 theString.value->markUnshareable(); return &(theString.value->data[charIndex]); }
2). 将proxy class传递给接受"references to non-const objects"的函数.
假设有一个swap函数用于对象两个char的内容:
void swap(char& a, char& b);
那么将无法将CharProxy做参数传递给swap,因为swap的参数是char&,尽管CharProxy可以转换到char,但由于抓换后的char是临时对象,仍然无法绑定到char&,解决方法似乎只有对swap进行重载.
3). 通过proxy cobjects调用真实对象的member function.
如果proxy class的作用是用来取代内置类型,那么它必须也应该对内置类型能够进行的操作进行重载,如++,+=等,如果它用来取代类类型,那么它也必须具有相同成员函数,使得对该类类型能够进行的操作同样也能够施行于proxy class.
4). 隐式类型转换.
proxy class要具有和被代理类型相同的行为,通常的做法是重载隐式转换操作符,正如条款5对proxy class的使用那样,proxy class可以利用"用户定制的隐式类型转换不能连续实行两次"的特点阻止不必要的隐式类型转换,proxy class同样可能因为这个特点而阻止用户需要的隐式类型转换.
5. 评估
proxy class的作用很强大,像上面所提到的实现多维数组,区分operator[]的读写操作,压抑隐式类型转换等,但是也有其缺点,如果函数返回proxy class对象,那么它僵尸一个临时对象,产生和销毁它就有可能带来额外的构造和析构成本,此外正如4所讲,proxy class无法完全代替真正对象的行为,尽管大多数情况下真正对象的操作都可由proxy class完成.