30 Proxy classes(替代类、代理类)

  • 实现二维数组

C++中如何支持多维数组:产生一个class,用以表现我们需要却被语言遗漏的对象,我们可以这样定义一个class template:

template
class Array2D
{
public:
	Array2D(int dim1,int dim2);
	...
};

我们可以这样定义数组:

Array2D data(10,20);
Array2D* data = new Array2D(10,20);

void processInput(int dim1,int dim2)
{
	Array2D data(dim1,dim2);
	...
}

但是,在数组对象的使用并不十分直截了当,为了保持C和C++一致性,我们希望能够方括号表现数组索引cout << data[3][6],我们必须重载operator[][],但是C++中并没有operator[][]操作符。

或许愿意以括号表现数组索引,必须重载operator(),但是这样做起来,必须这样使用:cout<

当调用data[10][20],变量data并非真正的二维数组,它其实是10个元素组成的一维数组,每个元素本身又是20个元素所组成的一维数组。

我们可以在Array2D class中玩相同把戏:将operator[]重载,令它返回一个Array1D对象。然后我们再对Array1D重载operator[],令它返回原来二维数组中的一个元素:

template
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 data(10,20);
...
cout << data[3][6];

每个Array1D对象象征着一个一维数组,观念上它并不存在与Array2D的用户心中,凡“用来代表(象征)其他对象”,ch昂被称为proxy objects,而用以表现proxy objects者,我们称为proxy classes。proxy objects有时候被称为surrogates(替代品)。

  • 区分operator[] 的读写动作

proxy classes用途很多,例如用来阻止单参constructors执行隐式转换,在proxy classes各种用于中,最明显的就是区分operator[]的读写动作了。

考虑上章reference-counted字符串类型,它支持operator[]。我们即将产生proxies来区别读写动作,对于一个proxy,你只有3件事情可做:

  • 产生它,本例也就是它代表哪一个字符串的的哪一个字符
  • 以它作为赋值动作的目标(接收端),如果这么使用,proxy代表的将是“调用operator[]函数”的那个字符串的左值运用
  • 以其他方式使用之。如果这么使用,proxy表现的是“调用operatorp[]函数”的那个字符串的右值运用

下面是一个reference-counted String class。其中利用proxy class来区分operator[]的左值运用和右值运用:

class String 	//reference-counted strings
{
public:
	class CharProxy
	{
	public:
		CharProxy(String& str,int index);				//构造
		CharProxy& operator = (const CharProxy& rhs);	//左值运算
		CharProxy& operator = (char c);					//右值运算
		
		operator char() const;
		
	private:
		String& theString;						//这个proxy附属的字符串
		int charIndex;							//这个proxy所代表的字符
	};
	
	const CharProxy operator[](int index) const; //针对const strings
	CharProxy operator[](int index);			 //针对non-const strings
	...
	
	friend class CharProxy;
	
private:
	RCPtr value;
};

除了增加了CharProxy class以外,这个String class和上章那个String class之间唯一的区别是,此处的两个operator[]都返回CharProxy对象:

String s1,s2;
...
cout << s1[5];    //合法
s2[5] = 'x';	  //合法
s1[3] = s2[8];    //合法

有趣的不是它能够有效运作,有趣的是它如何运作:

cout << s1[5];

表达式s1[5]产生一个CharProxy对象,此对象没有定义output操作符,所以寻找隐式转换,使用operator char() const来执行,于是CharProxy所表现的字符串被打印出来,这是典型的CharProxy-to-char转换,发生在所有“被用来作为右值”的CharProxy对象身上。

左值运算处理方式又不同:

s2[5] = 'x';

表达式s2[5]导出一个CharProxy,但这次调用的对象assignment动作的操作符。

同样道理,以下句子:

s1[3] = s2[8];

调用的是两个CharProxy对象的assignment操作符,在该操作符中我们知道左端对象用作左值,右端对象用作右值。

下面是String operator[]实现:

const String::CharProxy String::operator[](int index) const
{
	return CharProxy(const_cast*this,index);
}

 String::CharProxy String::operator[](int index)
 {
	 return CharProxy(*this,index);
 }

每个函数都只是产生并返回“被请求字符”的一个proxy。我们延缓此等行为,直到直到该行为是读取或者写。

operator[]返回的每一个proxy都会记住它所附属的字符串,以及它所在的索引位置:

String::CharProxy::CharProxy(String& str,int index)
	:theString(str),charIndex(index){}

将proxy转换为右值,是非常直接了当的事情——我们只需要返回该proxy所表现的字符串副本就行:


String::CharProxy::operator char() const
{
	return theString.value->data[index];
}

接下来实现CharProxy的assignment操作符。其间必须面对一个事实:proxy所代表的字符将被作为赋值动作的目标,也就是一个左值:

String::CharProxy&
String::CharProxy::operator = (const CharProxy& rhs)
{
	//如果字符串与其他String对象共享实值
	//将实值复制一份,供本字符串单独使用
	if(theString.value->isShared())
		theString.value = new StringValue(theString.value->data);
	
	//现在进行赋值动作:将rhs所代表的字符值
	//赋值*this所代表的字符
	theString.value->datap[charIndex] = 
				rhs.theString.value->data[rhs.charIndex];
	return *this;

第二个CharProxy assignment操作符和上述版本几乎雷同:

CharProxy& String::CharProxy::operator = (char c)
{
	if(theString.value->isShared())
		theString.value = new StringValue(theString.value->data);
	
	theString.value->data[charIndex] = c;
	return *this;
}
  • 限制

Proxy class很适合区分operator[]的左值和右值运算,但是对象也可能在其他情况下被当做左值使用,如果String::operator[]返回时个CharProxy而非char&将不能编译通过,因为“对proxy取址所获取的指针和对真实对象取址获取的指针类型不同”如下代码:

String s1 = "Hello";
char* p = &s1[1];

为了消除这个难点,我们需要在CharProxy class内将取址操作符重载:

class String
{
public:
	class CharProxy
	{
	public:
		...
		char* operator&();
		const char* operator() const;
		...
	};
};

实现如下:

const char* String::CharProxy::operator() const
{
	return &(theString.value->data[charIndex]);
}

char* String::CharProxy::operator&()
{
	//确定“标的字符”所属的字符串实值不为任何其他
	//String对象共享
	if(theString.value->isShared())
		theString.value = new StringValue(theString.value->data);
	
	//我们不知道clients会将本函数返回的指针保存多久,所以
	//“目标字符”所属的字符串绝不可以被共享
	theString.value->markUnshanreable();
	
	return &(theString.value->data[charIndex]);
}

如果我们有一个template用来实现reference-counted数组,其中利用proxy classes来区分operator[]被调用时的左值运算和右值运算,那么chars和其替代CharProxys第二个不同点是不支持+= ++这样的运算符,需要在下面的Proxy类中重载:

template
class Array
{
public:
	class Proxy
	{
	public:
		Proxy(Array& array,index);
		Proxy& operator = (const T& rhs);
		operator T() const;
		...
	};
};

Array intArray;
...
intArray[5] = 22;	//没问题
intArray[5] += 5;	//错误
intArray[5]++;		//错误

另一个问题就是“通过proxies调用真实对象的member functions”,如果你直接那么做,会失败。

class Rational
{
public:
	Rational(int numerator = 0,int denominator = 1);
	int numerator() const;
	int denominator() const;
	...
};

Array array;

cout << array[4].numerator();			//错误
int denom = array[22].denominator();	//错误

原因:operator[]返回的是一个proxy对象而不是实际的Rational对象。成员函数只存在于Rational对象上,如果你想成功必须对proxy对这两个函数重载。

另一个proxy对象替代对象失败的情况作为非const的引用传递给函数:

void swap(char& a,char& b);

String s = "+C+";
swap(s[0],s[1]);		//有问题

String::operator[]返回的是CharProxy对象,但swap却要求的使用char&,一个CharProxy对象可以隐式转换成char并不能转换成char&,因为这个char是一个临时对象(它是operator()返回值),所以拒绝将临时对象绑定为非const的引用形参是有原因的。

最后一种proxy对象不能无缝替换成实际对象的情况是隐式类型转换。当proxy对象隐式转换成它所扮演的实际对象时候,一个用户自定义转换函数被调用了。例如CharProxy对象可以通过operator()转换成char。编译器在调用函数而将参数转换成该函数需要的类型时,只调用了用户自定义类型的转换函数。于是,很有可能在函数调用的时候,传实际对象成功而传proxy对象失败。如下代码:

class TVStation
{
public:
	TVStation(int channel);
	...
};

void watchTV(const TVStation& station,float hourToWatch);


//借助int到TVStation隐式类型转换
watchTV(10,2.5);		//成功

Array intArray;
intArray[4] = 10;
watchTV(intArray[4],2.5);	//失败
  • 总结:

Proxy类可以用来完成一些其他方法很难甚至不可能实现的问题:多维数组、区分左值和右值,限制隐式类型转换。

 同时,proxy类也有缺点,作为函数的返回值,proxy对象是临时对象,它必须被构造和析构,这不是免费的。Proxy增加的软件的复杂度,因为额外增加的类使得事情更难设计、实现、理解和维护。

最后,从一个实际对象类改换到处理proxy对象类改变的类的语意。因为proxy对象通常表现出的行为和实际对象的微妙区别,需要针对proxy对象进行隐式类型转换。

 

你可能感兴趣的:(More,Effective,C++读书笔记)