使用代理类区分operator[]进行的是读操作还是写操作 — 懒惰计算思想的运用

扮演其它对象的对象通常被称为代理类

在代理类的各种用法中,最神奇的是帮助区分通过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 value; 
}; 


const String::CharProxy String::operator[](int index) const 

  return CharProxy(const_cast(*this), index); 


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类定义所有这些函数。这是一个极大量的工作,你可能不愿意去做的。不幸的是,你要么去做这些工作,要么没有这些操作,不能两全。 


一个类似的问题必须面对:通过proxy对象调用实际对象的成员函数。想避开它是不
可能的。例如,假设我们用带引用计数的数组处理有理数。我们将定义一个Rational类,
然后使用前面看到的Array模板:  
class Rational { 
public: 
  Rational(int numerator = 0, int denominator = 1); 
  int numerator() const; 
  int denominator() const; 
  ... 
}; 


Array 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 

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; 
intArray[4] = 10; 
watchTV(intArray[4], 2.5);              // error! no conversion 
                                        // from Proxy to 
                                        // TVStation 


由于问题发生在隐式类型转换上,它很难解决。实际上,更好的设计应该是申明它的
构造函数为explicit,以使得第一次的调用watchTV()的行为都编译失败。

你可能感兴趣的:(C++语法)