扮演其它对象的对象通常被称为代理类
在代理类的各种用法中,最神奇的是帮助区分通过operator[]进行的是读操作还是写操作
我们想区分将operator[]用作左值还是右值,因为,对于有引用计数的数据结构,读
操作的代价可以远小于写操作的代价。前面所讲的引用计数的使用,引用计数对象的写操作将导致整个数据结构的拷贝,而读不需要,只要简单地返回一个值。不幸的是,在operator[]内部,没有办法确定它是怎么被调用的,不可能区分出它是做左值还是右值。
我们的方法基于这个事实:也许不可能在operator[]内部区分左值还是右值操作,但
我们仍然能区别对待读操作和写操作,如果我们将判断读还是写的行为推迟到我们知道
operator[]的结果被怎么使用之后的话。 我们所需要的是有一个方法将读或写的判断推迟到operator[]返回之后。 (这是lazy原则 )
proxy类可以让我们得到我们所需要的时机,因为我们可以修改operator[]让它返回
一个(代理字符的)proxy对象而不是字符本身。我们可以等着看这个proxy怎么被使用。如果是读它,我们可以断定operator[]的调用是读。如果它被写,我们必须将operator[]的调用处理为写。
我们马上来看代码,但首先要理解我们使用的proxy类。在proxy类上只能做三件事:
1、创建它,也就是指定它扮演哪个字符。
2、将它作为赋值操作的目标,在这种情况下可以将赋值真正作用在它扮演的字符上。
这样被使用时,proxy类扮演的是左值。
3、用其它方式使用它。这时,代理类扮演的是右值。
这里是一个被带引用计数的string类用作proxy类以区分operator[]是作左值还是右
值使用的例子:
class String { // reference-counted strings;
public: // see Item 29 for details
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; // rvalue
// use
char * operator&();
const char * operator&() const;
private:
String& theString; // string this proxy pertains to
int charIndex; // char within that string
// this proxy stands for
};
// continuation of String class
const CharProxy
operator[](int index) const; // for const Strings
CharProxy operator[](int index); // for non-const Strings
...
friend class CharProxy;
private:
RCPtr
};
const String::CharProxy String::operator[](int index) const
{
return CharProxy(const_cast
}
String::CharProxy String::operator[](int index)
{
return CharProxy(*this, index);
}
String::CharProxy::CharProxy(String& str, int index)
: theString(str), charIndex(index) {}
String::CharProxy::operator char() const
{
return theString.value->data[charIndex];
}
String::CharProxy&
String::CharProxy::operator=(const CharProxy& rhs)
{
// if the string is sharing a value with other String objects,
// break off a separate copy of the value for this string only
if (theString.value->isShared()) {
theString.value = new StringValue(theString.value->data);
}
// now make the assignment: assign the value of the char
// represented by rhs to the char represented by *this
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;
}
const char * String::CharProxy::operator&() const
{
return &(theString.value->data[charIndex]);
}
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类(我们将在下面讲解)外,这个String类与前面引用计数的使用中的最终版本相比,唯一不同之处就是所有的operator[]函数现在返回的是CharProxy对象。然而,String类的用户可以忽略这一点,并当作operator[]返回的仍然是通常形式的字符(或其引用)来编程:
String s1, s2; // reference-counted strings
// using proxies
...
cout << s1[5]; // still legal, still works
s2[5] = 'x'; // also legal, also works
s1[3] = s2[8]; // of course it's legal,
// of course it works
有意思的不是它能工作,而是它为什么能工作。
先看这条语句:
cout << s1[5];
表达式s1[5]返回的是一CharProxy对象。没有为这样的对象定义输出流操作,所以编
译器努力地寻找一个隐式的类型转换以使得operator<<调用成功 。它们找到一个:在CahrProxy类内部申明了一个隐式转换到char的操作。于是自动调用这个转换操作,结果就是CharProxy类扮演的字符被打印输出了。这个CharProxy到char的转换是所
有代理对象作右值使用时发生的典型行为。
作左值时的处理就不一样了。再看:
s2[5] = 'x';
和前面一样, 表达式s2[5]返回的是一个CharProxy对象, 但这次它是赋值操作的目标。
由于赋值的目标是CharProxy类,所以调用的是CharProxy类中的赋值操作。这至关重要,因为在CharProxy的赋值操作中, 我们知道被赋值的CharProxy对象是作左值使用的。 因此,我们知道proxy类扮演的字符是作左值使用的, 必须执行一些必要的操作以实现字符的左值操作。
同理,语句
s1[3] = s2[8];
调用作用于两个CharProxy对象间的赋值操作,在此操作内部,我们知道左边一个是
作左值,右边一个作右值 。
局限性:
当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()的返回值) ,拒绝将临时对象绑定为非const的引用的形参是有道理的。
最后一种proxy对象不能无缝替换实际对象的情况是隐式类型转换。当proxy对象隐
式转换为它所扮演的实际对象时,一个用户自定义的转换函数被调用了。例如,一个
CharProxy对象可以转换为它扮演的char,通过调用operator char()函数。编译器在调用函数而将参数转换为此函数所要的类型时,只调用一个用户自定义的转换函数。于是,很可能在函数调用时,传实际对象是成功的而传proxy对象是失败的。例如,
我们有一个TVStation类和一个函数watchTV():
class TVStation {
public:
TVStation(int channel);
...
};
void watchTV(const TVStation& station, float hoursToWatch);
借助于int到TVStation的隐式类型转换 ,我们可以这么做:
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()的行为都编译失败。