分类: C/C++
2009-08-26 15:30:04
l 局限性
使用proxy类是个区分operator[]作左值还是右值的好方法,但它不是没有缺点的。我们很喜欢proxy对象对其所扮演的对象的无缝替代,但这很难实现。这是因为,右值不只是出现在赋值运算的情况下,那时,proxy对象的行为就和实际的对象不一致了。
再看一下来自于Item M29的代码,以证明我们为什么为每个StringValue对象增加一个共享标志。如果String::operator[]返回一个CharProxy而不是char &,下面的代码将不能编译:
String s1 = "Hello";
char *p = &s1[1]; // error!
表达式s1[1]返回一个CharProxy,于是“=”的右边是一个CharProxy *。没有从CharProxy *到char *的转换函数,所以p的初始化过程编译失败了。通常,取proxy对象地址的操作与取实际对象地址的操作得到的指针,其类型是不同的。
要消除这个不同,你需要重载CharProxy类的取地址运算:
class String {
public:
class CharProxy {
public:
...
char * operator&();
const char * operator&() const;
...
};
...
};
这些函数很容易实现。const版本返回其扮演的字符的const型的指针:
const char * String::CharProxy::operator&() const
{
return &(theString.value->data[charIndex]);
}
非const版本有多一些操作,因为它返回的指针指项的字符可以被修改。它和Item M29中的非const的String::operator[]行为相似,实现也很接近:
char * String::CharProxy::operator&()
{
// make sure the character to which this function returns
// a pointer isn't shared by any other String objects
if (theString.value->isShared()) {
theString.value = new StringValue(theString.value->data);
}
// we don't know how long the pointer this function
// returns will be kept by clients, so the StringValue
// object can never be shared
theString.value->markUnshareable();
return &(theString.value->data[charIndex]);
}
其代码和CharProxy的其它成员函数有很多相同,所以我知道你将把它们封入一个私有成员函数。
char和代理它的CharProxy的第二个不同之处出现带引用计数的数组模板中如果我们想用proxy类来区分其operator[]作左值还是右值时:
template
class Array { // using proxies
public:
class Proxy {
public:
Proxy(Array
Proxy& operator=(const T& rhs);
operator T() const;
...
};
const Proxy operator[](int index) const;
Proxy operator[](int index);
...
};
看一下这个数组可能被怎样使用:
Array
...
intArray[5] = 22; // fine
intArray[5] += 5; // error!
++intArray[5]; // error!
如我们所料,当operator[]作最简单的赋值操作的目标时,是成功的,但当它出现operator+=和operator++的左侧时,失败了。因为operator[]返回一个proxy对象,而它没有operator+=和operator++操作。同样的情况存在于其它需要左值的操作中,包括operator*=、operator<<=、operator--等等。如果你想让这些操作你作用在operator[]上,必须为Arrar
一个类似的问题必须面对:通过proxy对象调用实际对象的成员函数。想避开它是不可能的。例如,假设我们用带引用计数的数组处理有理数。我们将定义一个Rational类,然后使用前面看到的Array模板:
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
...
};
Array
这是我们所期望的使用方式,但我们很失望:
cout << array[4].numerator(); // error!
int denom = array[22].denominator(); // error!
现在,不同之处很清楚了;operator[]返回一个proxy对象而不是实际的Rational对象。但成员函数numerator()和denominator()只存在于Rational对象上,而不是其proxy对象。因此,你的编译器发出了抱怨。要使得proxy对象的行为和它们所扮演的对象一致,你必须重载可作用于实际对象的每一个函数。
另一个proxy对象替代实际对象失败的情况是作为非const的引用传给函数:
void swap(char& a, char& b); // swaps the value of a and b
String s = "+C+"; // oops, should be "C++"
swap(s[0], s[1]); // this should fix the
// problem, but it won't
// compile
String::operator[]返回一个CharProxy对象,但swap()函数要求它所参数是char &类型。一个CharProxy对象可以印式地转换为一个char,但没有转换为char &的转换函数。而它可能转换成的char并不能成为swap的char &参数,因为这个char是一个临时对象(它是operator char()的返回值),根据Item M19的解释,拒绝将临时对象绑定为非const的引用的形参是有道理的。
最后一种proxy对象不能无缝替换实际对象的情况是隐式类型转换。当proxy对象隐式转换为它所扮演的实际对象时,一个用户自定义的转换函数被调用了。例如,一个CharProxy对象可以转换为它扮演的char,通过调用operator char()函数。如Item M5解释的,编译器在调用函数而将参数转换为此函数所要的类型时,只调用一个用户自定义的转换函数。于是,很可能在函数调用时,传实际对象是成功的而传proxy对象是失败的。例如,我们有一个TVStation类和一个函数watchTV():
class TVStation {
public:
TVStation(int channel);
...
};
void watchTV(const TVStation& station, float hoursToWatch);
借助于int到TVStation的隐式类型转换(见Item M5),我们可以这么做:
watchTV(10, 2.5); // watch channel 10 for
// 2.5 hours
然而,当使用那个用proxy类区分operator[]作左右值的带引用计数的数组模板时,我们就不能这么做了:
Array
intArray[4] = 10;
watchTV(intArray[4], 2.5); // error! no conversion
// from Proxy
// TVStation
由于问题发生在隐式类型转换上,它很难解决。实际上,更好的设计应该是申明它的构造函数为explicit,以使得第一次的调用watchTV()的行为都编译失败。关于隐式类型转换的细节和explicit对此的影响见Item M5。
l 评述
Proxy类可以完成一些其它方法很难甚至不可能实现的行为。多维数组是一个例子,左/右值的区分是第二个,限制隐式类型转换(见Item M5)是第三个。
同时,proxy类也有缺点。作为函数返回值,proxy对象是临时对象(见Item 19),它们必须被构造和析构。这不是免费的,虽然此付出能从具备了区分读写的能力上得到更多的补偿。Proxy对象的存在增加了软件的复杂度,因为额外增加的类使得事情更难设计、实现、理解和维护。