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

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;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值