More Effective C++ 条款30 Proxy classes(替身类,代理类)

1. 所谓代理类(proxy class),指的是"它的每一个对象都是为了其他对象而存在的,就像是其他对象的代理人一般".某些情况下用代理类取代某些内置类型可以实现独特的功能,因为可以为代理类定义成员函数而但却无法对内置类型定义操作.条款5就展示了一个使用代理类阻止隐式类型转换的例子.

2. 实现二维数组.

    C++没有提供分配动态二维数组的语法,因此常常需要定义一些类(模板实现这些功能),像这样:

template<class T>
class Array2D {
public:
    Array2D(int dim1, int dim2);
    ...
};
View Code

    既然是二维数组,那么有必要提供使用"[][]"访问元素的操作,然而[][]并不是一个操作符,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;
    ...
};
View Code

    那么以下操作:

Array2D<float> data(10, 20);
...
cout << data[3][6];
View Code

   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
};
View Code

    对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;
}
//以上来那个函数的代码部分有重复,可考虑将重复部分提取成一个函数
View Code

    由于内存开辟和数据结构赋值任务交由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);
}
View Code

    CharProxy实现的其他部分如下:

String::CharProxy::CharProxy(String& str, int index): theString(str), charIndex(index) {}
String::CharProxy::operator char() const
{
return theString.value->data[charIndex];
}
View Code

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;
        ...
    };
    ...
};
View Code

    const版本operator&实现比较容易:

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

    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]);
}
View Code

    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完成.

原文地址:https://www.cnblogs.com/reasno/p/4858490.html