C++ 简易string类实现(六)-真正的写时复制

来源:互联网 发布:c程序员面试题库 编辑:程序博客网 时间:2024/06/01 17:26

在C++ 简易string类实现(二)-引用计数中,我们引入了写时复制(copy on write),但因为C++编译期无法告诉我们operator[]是被用于读取或写,出于安全,这里假设对non-const 的operator[]的调用都是写操作,这样虽然能够正常运行,但是诸如下面的代码:

String str1 = "123";std::cout << str1[2];   //读操作str1[1] = 2;            //写操作

第2行里,明显是一个读操作,但是为了安全,也会被假设在写操作.这样使用上固然没问题,但是依然不是真正的写时复制(copy on write),那么有没有办法使得编译期针对operator[],可以”智能”地知道,何时是写操作,何时是读操作吗?

下面是C++ 简易string类实现(二)-引用计数中String类的non-const版的operator[]:

char& String::operator[](size_t index_)

返回类型是char&,这里的问题是,对于写操作或者读操作,char&没有有效的办法做区分.在 C++ 简易string类实现(五)-进一步抽象中,我们引入CountHolder的一个思想是,”加上一层间接性”.这里我们同样加入一层间接性,将返回的char类型包装是一个类,我们知道,对于一个类(而不是其成员函数的返回类型),可以很容易知道其调用是写操作还是读操作(利用所调用的成员函数区分).基于此,我们引入proxy class(代理类),令String的operator返回是字符串中字符的proxy,而不返回字符本身.然后我们可以等待,看看这个proxy如何被调用.如果它被读,我们可以(有点过时地)将operator[]的调用动作视为一个读取动作.如果它被写,我们必须将operator[]的调用视为一个写操作.

对于即将使用的proxy,有3件事需呀做:
(1)产生它,本例也就是指定它代表哪一个字符串中的哪一个字符;
(2)以它作为赋值动作(assignment)的目标(接受端),这种情况下,是对它所代表的字符串内的字符做赋值动作.如果这么使用,proxy代表的将是”调用operator[]函数”的那个字符串的左值运用.
(3)以其它方式使用之.如果这么使用,proxy表现的是”调用operator[]函数”的那个字符串的右值运用.

下面是一个带有引用计数特性的String class,其中利用proxy class 来区分operator[]的左值运用(写操作)和右值运用(读操作):

class String{    friend class CharProxy;    //访问String私有变量public:    String(const char* str_ = "");    class CharProxy    {    public:        CharProxy(String& str_, int index_);        //构造        CharProxy& operator=(const CharProxy& rhs_);    //左值运用        CharProxy& operator=(char ch_);     //左值运用        operator char() const;      //右值运用    private:        //记录所引用的String对象和当前字符的下标,        //在左值运用时,可以修改String对象的值        String& _theString;        int _charIndex;    };    const CharProxy operator[](int index) const;    CharProxy operator[](int index);private:    struct StringValue : public RCObject    {        char* ptr;        StringValue(const char* str_);        StringValue(const StringValue& rhs_);        ~StringValue();    private:        void init(const char* str_);    };    RCPtr<StringValue> _value;};

这里的String class和C++ 简易string类实现(四)-自动操作引用次数中的那个String class之间唯一的差别就是,此处的两个operator[]都返回CharProxy对象.然而对于用户可以忽略这点(对于某些操作,例如对CharProxy变量做&操作会有问题,下面会说到),犹如operator[]返回字符一样:

String str1 = "123";std::cout << str1[2];   //合法,有效运转str1[1] = 2;            //合法,有效运转

考虑这行代码

std::cout << str1[2];

表达式str1[2]产生一个CharProxy对象.此对象不曾定义output操作符,所以编译器赶紧寻找一个它能够实施的隐式类型转换,使得operator<<调用操作能够成功.编译器真的找到了一个:将CharProxy隐式转型为char.此转换函数声明于CharProxy函数内.于是编译器自动调用这个转换操作符,于是CharProxy所表现的字符串字符被打印出去.这是典型的CharProxy-to-char转换,发生在所有”被用来作为右值”的CharProxy对象身上.

左值运用的处理方式又不相同,例如这行代码

str1[1] = 2; 

和上例一样,表达式str1[1]导出一个CharProxy对象,但这次这个对象是assignment动作的目标物.会调用哪个assignment操作符呢?由于目标物是个CharProxy,所以被调用的assignment操作符会是CharProxy class所定义的那个.这很重要,因为在CharProxy的assignment操作符内,我们确知”被赋值的CharProxy对象被用来作为一个左值”.我们因此知道proxy所代表的那个”字符串内的字符”将被用来作为一个左值,并因此采取所有必要行动,对该字符实施左值处理.

即使是以下这段代码:

String s1 = "1234";String s2 = "12333";s1[3] = s2[3]; 

最后一行调用的两个CharProxy对象的assignment操作符,在该操作符中我们知道左端对象被用来作为一个左值,右端对象被用来作为一个右值.

对于String的operator[]

const CharProxy operator[](int index) const;CharProxy operator[](int index);

每个函数都只是产生并返回”被请求字符”的一个proxy.没有任何动作施加于此字符身上:我们延缓此等行为,直到知道该行为是”读取”还是”写”.

虽然Proxy class很适合用来区分operator[]的左值运用和右值运用,但是这项技术并非没有缺点.我们希望proxy object能够无间隙地取代它们所代表的对象,但是这样的想法却很难达成.因为除了赋值(assignment)之外,对象亦有可能在其它情境下被当做左值使用,而在那种情况下的proxies常常会有与真实对象不同的行为.

例如下述代码:

String s1 = "123456";char* p = &s2[1];   //错误,无法通过编译

第二行代码无法通过编译的原因是,&s2[1]返回的是一个CharProxy*类型指针,由于不存在任何函数可以将CharProxy * 转换为 char *,所以上述的p的初始化动作无法通过编译.一般而言,”对proxy取址”所获得的指针类型和”对真实对象地址”所获得的指针类型不同.

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

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

对应的实现为:

char* String::CharProxy::operator&(){    //确定"标记的字符"(本函数返回一个指针指向它)所属的字符串实值不为    //任何其它String对象共享(如果共享,就做一份专属副本出来)    if (_theString._value->isShared())    {        _theString._value = new StringValue(_theString._value->ptr);    }    //我们不知道clients会将本函数返回的指针保留多久,所以"目标字符"    //所属的字符串实值(一个StringValue对象)绝不可以被共享    _theString._value->markUnshareable();    return &(_theString._value->ptr[_charIndex]);}const char* String::CharProxy::operator&() const{    return &(_theString._value->ptr[_charIndex]);}

此时,

String s1 = "123456";char* p = &s2[1];   //编译通过

问题得以解决,但本质上,CharProxy和char是不同的,我们可以通过重载的方式解决遇到的问题,这样无疑增加了工作量,但现在我们在copy-on-copy上的工作已经做到很好了,这些工作量的增加还是可以接受的,当然可以通过将一些通用的运算符加以封装,得以复用,然后针对具体的类,再增加额外所需的函数.

完整代码如下,
结构图:
这里写图片描述

class RCObject{public:    RCObject();    RCObject(const RCObject& rhs_);    //使RCObject成为抽象基类,但该纯虚函数需要提供    //定义,不然会使被继承的类无法在栈上创建(原因可    //查阅如何仅在堆上或栈上分配内存)    virtual ~RCObject() = 0;       public:    void addReference();    void removeReference();    void  markUnshareable();    bool isShareable() const;    bool isShared() const;private:    RCObject& operator=(const RCObject&) = delete;private:    int refCount;    bool shareable;};template<typename T>class RCPtr{public:    RCPtr(T* realPtr = 0);    RCPtr(const RCPtr& rhs_);    RCPtr& operator=(const RCPtr& rhs_);    ~RCPtr();public:    T* operator->() const;    T& operator*() const;private:    void init();private:    T* _ptr;};class String{    friend class CharProxy;public:    String(const char* str_ = "");    class CharProxy    {    public:        CharProxy(String& str_, int index_);        //构造        CharProxy& operator=(const CharProxy& rhs_);    //左值运用        CharProxy& operator=(char ch_);     //左值运用        operator char() const;      //右值运用        char* operator&();        const char* operator&() const;    private:        //记录所引用的String对象和当前字符的下标,        //在左值运用时,可以修改String对象的值        String& _theString;        int _charIndex;    };    const CharProxy operator[](int index) const;    CharProxy operator[](int index);private:    struct StringValue : public RCObject    {        char* ptr;        StringValue(const char* str_);        StringValue(const StringValue& rhs_);        ~StringValue();    private:        void init(const char* str_);    };    RCPtr<StringValue> _value;};
RCObject::RCObject()    :refCount(0), shareable(true)   //refCount初始化为0,其值完全有RCPtr控制{}RCObject::RCObject(const RCObject&)//调用无参构造函数,注意:该调用仅能在//初始化成员列表里,如果在函数实现内调用,//那么仅仅是在栈上生成新的对象,而不是完成//该对象的成员初始化    :RCObject(){    std::cout << "RCObject" << std::endl;}RCObject::~RCObject(){    //std::cout << "~RCObject" << std::endl;}void RCObject::addReference(){    ++refCount;}void RCObject::removeReference(){    if (--refCount == 0)    {        delete this;    }}void RCObject::markUnshareable(){    shareable = false;}bool RCObject::isShareable() const{    return shareable;}bool RCObject::isShared() const{    return refCount > 1;}template<typename T>RCPtr<T>::RCPtr(const RCPtr& rhs_)    : _ptr(rhs_._ptr){    init();}template<typename T>void RCPtr<T>::init(){    if (_ptr == nullptr)    {        return;    }    if (_ptr->isShareable() == false)    {        //如果其值不可共享,那么就赋值一份        //注意,这里将新对象的赋值行为,由之前的        //String转移到RCPtr,此时T(在原有的String实现中是StringValue)        //若要完成深拷贝,T(StringValue)必须重写赋值运算符        _ptr = new T(*_ptr);    }    //计数器完全由RCPtr控制,即使上个if语句内重新赋值的对象    //其引用计数也由RCPtr控制,故将RCObject的refCount初值赋值0    _ptr->addReference();   }template<typename T>RCPtr<T>& RCPtr<T>::operator=(const RCPtr& rhs_){    if (_ptr != rhs_._ptr)    {        if (_ptr != nullptr)        {            _ptr->removeReference();        }        _ptr = rhs_._ptr;        init();    }    return *this;}template<typename T>RCPtr<T>::~RCPtr(){    if (_ptr != nullptr)    {        _ptr->removeReference();    }}template<typename T>T* RCPtr<T>::operator->() const{    return _ptr;}template<typename T>T& RCPtr<T>::operator*() const{    return *_ptr;}const String::CharProxy String::operator[](int index_) const{    //为了简洁,这里略去对下标的安全检查    return CharProxy(const_cast<String&>(*this), index_);}String::CharProxy String::operator[](int index_){    //将决定读写的权利交给CharProxy,这里当做读,简单的返回    //CharProxy实例    return CharProxy(*this, index_);}String::CharProxy::CharProxy(String& str_, int index_)    : _theString(str_), _charIndex(index_){}String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs_){    if (_theString._value->isShared())    {        _theString._value = new String::StringValue(_theString._value->ptr);    }    //不再需RCObject中的shareable变量及对应的成员函数    _theString._value->ptr[_charIndex] =        rhs_._theString._value->ptr[rhs_._charIndex];    return *this;}String::CharProxy& String::CharProxy::operator=(char ch_){    if (_theString._value->isShared())    {        _theString._value = new StringValue(_theString._value->ptr);    }    _theString._value->ptr[_charIndex] = ch_;    return *this;}String::CharProxy::operator char() const{    return _theString._value->ptr[_charIndex];}char* String::CharProxy::operator&(){    //确定"标记的字符"(本函数返回一个指针指向它)所属的字符串实值不为    //任何其它String对象共享(如果共享,就做一份专属副本出来)    if (_theString._value->isShared())    {        _theString._value = new StringValue(_theString._value->ptr);    }    //我们不知道clients会将本函数返回的指针保留多久,所以"目标字符"    //所属的字符串实值(一个StringValue对象)绝不可以被共享    _theString._value->markUnshareable();    return &(_theString._value->ptr[_charIndex]);}const char* String::CharProxy::operator&() const{    return &(_theString._value->ptr[_charIndex]);}void String::StringValue::init(const char* str_){    ptr = new char[strlen(str_) + 1];    strcpy(ptr, str_);}String::StringValue::StringValue(const StringValue& rhs_){    init(rhs_.ptr);}String::StringValue::StringValue(const char* str_){    init(str_);}String::StringValue::~StringValue(){    if (ptr != nullptr)    {        delete[] ptr;    }}
原创粉丝点击