前言
C++无法自由地创建多维数组,你可能会作出反驳:1
int data[10][20];
但我指的是以变量作为构建数组的形参:1
2
3
4void processInput(int dim1, int dim2){
int data[dim1][dim2];//error
int *data = new int[dim1][dim2];//error
}
实现二维数组
为了实现二维数组,我们不得不自己新建一个class,以期待完成功能:1
2
3
4
5
6template<class T>
class Array2D {
public:
Array2D(int dim1, int dim2);
...
};
遗憾的是Array2D生成的对象并不具备索引功能。也就是说:1
2Array2D<int> data(10, 20);//ok
cout << data[3][6];//error
我们只能最多重载一个operator[],不可能重载operator[][](详见More Effective C++ 7)。
事实上,我们完全有理由相信,一个二维数组在调用operator[]后应当返回一个Array1D对象,然后我们通过重载Array1D的operator[]即可顺利完成索引操作:1
2
3
4
5
6
7
8
9
10
11
12
13
14template<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];//ok
Array2D的用户并不需要知道Array1D类的存在。这个背后的“一维数组”对象从概念上来说,并不是为 Array2D 类的用户而存在的。其用户编程时就象他们在使用真正的二维数组一样
每个Array1D对象扮演的是一个一维数组,而这个一维数组并不存在于使用Array2D的程序中。“用来代表其他对象”的对象通常被称为代理类。Array1D就是一个代理类,它的对象扮演的是一个在概念上不存在的一维数组。
区分opeator[]的读写操作
我们已经了解operator[]会在两种不同的情况下调用:读和写。读是一个右值操作而写是一个左值操作。
在实现引用计数时我们假设所有operator[]操作均为写,这直接导致了一系列麻烦的操作,以及shareable标志位的诞生。
事实上我们可以通过Proxy class区分operato[]的操作,其原理为:将判断读和写的行为推迟到我们明确operator[]的结果会被如何使用。显然,这是Lzay evaluation的一大体现。
String中的proxy对象
结合String实例,我们可以修改operator[],令其返回一个proxy对象而非字符本身,然后观察proxy会被如何使用。
String中的proxy对象只能做3件事:
- 创建,指定扮演哪个字符
- 将其作为赋值的目标,此时扮演左值
- 以其他方式使用,扮演右值
以带有引用计数的String class为例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class String {
public:
class CharProxy { // proxies for string chars
public:
CharProxy(String& str, int index);
CharProxy& operator=(const CharProxy& rhs);
CharProxy& operator=(char c);
operator char() const;
private:
String& theString;
int charIndex;
};
const CharProxy operator[](int index) const;
CharProxy operator[](int index);
...
friend class CharProxy;
private:
RCPtr<StringValue> value;
};
当前String的operator[]函数将返回的是CharProxy对象。然而,String类的用户并不需要了解这一点:1
2
3
4
5String s1, s2;
...
cout << s1[5];
s2[5] = 'x';
s1[3] = s2[8];
右值操作
1 | cout << s1[5]; |
表达式s1[5]返回的是一个CharProxy对象。由于Proxy对象并没有定义IO流操作,于是编译器开始试图寻找令该语句编译成功的方法,最终找到了隐式转换。以上是代理类被作为右值操作时的常规行为。
左值操作
1 | s2[5] = 'x'; |
表达式s2[5]返回的是一个CharProxy对象,但这次它是赋值操作的目标。由于赋值的目标是CharProxy 类,因此调用的是harProxy类中的赋值操作。这至关重要,因为当进入Proxy对象的赋值函数时,我们明确当前String的operator[]执行了左值操作,因此有必要执行某些操作保证程序正常运行。
String的operator[]实现
1 | const String::CharProxy String::operator[](int index) const{ |
每个函数都创建和返回一个proxy对象来代替字符。根本没有对那个字符作任何操作:我们将它推迟到直到我们知道是读操作还是写操作。
需要注意的是const版本返回一个const Proxy对象,因此它不能被赋值,这正是我们想要的。并且在该函数中为了与构造函数匹配,我们使用了类型转换,此处类型转换构造的对象也是const,不用担心数据被篡改的问题。
Proxy对象的实现
构造与转换
通过operator[]返回的proxy对象记录了它属于哪个string,以及下标。当proxy对象作为右值被使用时,其返回值是一个不可修改的proxy对象:1
2
3
4
5String::CharProxy::CharProxy(String& str, int index)
: theString(str), charIndex(index) {}
String::CharProxy::operator char() const{
return theString.value->data[charIndex];
}
赋值操作
赋值操作的实现如下:1
2
3
4
5
6
7
8String::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;
}
以char作为形参执行的赋值操作也大同小异:1
2
3
4
5
6
7String::CharProxy& String::CharProxy::operator=(char c){
if (theString.value->isShared()) {
theString.value = new StringValue(theString.value->data);
}
theString.value->data[charIndex] = c;
return *this;
}
当然,我们应当编写一个private member function消除它们之间的代码重复。
Porxy class的局限性
Proxy并非完美,在某些场合下它离无缝替代差的很远。
取址
1 | String s1 = "Hello"; |
我们没法把一个charProxy*赋值给char*,要解决的话只能重载取址运算符。
const版本很容易实现:1
2
3const char * String::CharProxy::operator&() const{
return &(theString.value->data[charIndex]);
}
non-const版本则颇类似于前一节中的operator[]:1
2
3
4
5
6
7char * String::CharProxy::operator&(){
if (theString.value->isShared()) {
theString.value = new StringValue(theString.value->data);
}
theString.value->markUnshareable();//无法保证其被用于何种操作,因此锁定不可共享
return &(theString.value->data[charIndex]);
}
带引用计数的数组模板
如果我们想用proxy类来区分其operator[]作左值还是右值时:1
2
3
4
5
6
7
8
9
10
11
12
13
14template<class T>
class Array {
public:
class Proxy {
public:
Proxy(Array<T>& array, int index);
Proxy& operator=(const T& rhs);
operator T() const;
...
};
const Proxy operator[](int index) const;
Proxy operator[](int index);
...
};
该带有引用计数的数组可能被这样使用:1
2
3
4
5Array<int> intArray;
...
intArray[5] = 22;
intArray[5] += 5;//error!
++intArray[5];//error!
无法编译的原因在于我们没有为代理类重载这些操作符。相似的,我们也无法通过代理类来调用实际对象的member function,也没法作为非const的引用传给函数。
隐式类型转换
我们之所以能用代理类是因为它和真实对象间存在隐式转换,但当我们期待原对象发生隐式转换时,表达式将无法通过编译,原因在于隐式转换无法在同一时间触发多次。
总结
Proxy Class可以完成一些其它方法很难甚至不可能实现的行为,例如:
- 多维数组
- 区分左右值操作
- 限制隐式类型转换
Proxy Class亦有缺点,代理对象始终是一个临时对象,不可避免地存在着构造与析构的成本。