《More Effictive C++》学习笔记 — 技术(四)
条款29 —— 引用计数(二)
1、把引用计数加到既有的类上
我们将 CLS_RCObject 作为基类固然可以达到引用计数的目的。然而,对于已经存在的类(其他库中导出的类,框架中提供的类)等,我们该如何实现此功能?答案是提供间接性,增加一个代理类。新的类关系如下图:
(1)CLS_RCIPtr
CLS_RCIPtr 表示间接指针类。
template<typename T>
class CLS_RCIPtr
{
public:
CLS_RCIPtr(T* _ptr = 0);
CLS_RCIPtr(const CLS_RCIPtr& other);
~CLS_RCIPtr();
CLS_RCIPtr& operator=(const CLS_RCIPtr& other);
const T* operator->() const;
T* operator->();
const T& operator*() const;
T& operator*();
private:
struct CLS_CounterHolder : public CLS_RCObject
{
unique_ptr<T> pointer;
};
CLS_CounterHolder* pointerHolder;
void init();
void makeCopy();
};
template<typename T>
CLS_RCIPtr<T>::CLS_RCIPtr(T* _ptr) :
pointerHolder(new CLS_CounterHolder)
{
pointerHolder->pointer = _ptr;
init();
}
template<typename T>
CLS_RCIPtr<T>::CLS_RCIPtr(const CLS_RCIPtr& other) :
pointerHolder(other.pointerHolder)
{
init();
}
template<typename T>
CLS_RCIPtr<T>::~CLS_RCIPtr()
{
if (pointerHolder)
{
pointerHolder->removeReference();
}
}
template<typename T>
CLS_RCIPtr<T>& CLS_RCIPtr<T>::operator=(const CLS_RCIPtr& other)
{
if (pointerHolder != other.pointerHolder)
{
pointerHolder->removeReference();
pointerHolder = other.pointerHolder;
init();
}
return *this;
}
template<typename T>
const T* CLS_RCIPtr<T>::operator->() const
{
return pointerHolder->pointer;
}
template<typename T>
T* CLS_RCIPtr<T>::operator->()
{
makeCopy();
return pointerHolder->pointer;
}
template<typename T>
const T& CLS_RCIPtr<T>::operator*() const
{
return *(pointerHolder->pointer);
}
template<typename T>
T& CLS_RCIPtr<T>::operator*()
{
makeCopy();
return *(pointerHolder->pointer);
}
template<typename T>
void CLS_RCIPtr<T>::init()
{
if (pointerHolder == nullptr)
{
return;
}
if (pointerHolder->isShareable() == false)
{
auto temp = new CLS_CounterHolder;
temp->pointer.reset(new T(*(pointerHolder->pointer)));
pointerHolder = temp;
}
pointerHolder->addReference();
}
template<typename T>
void CLS_RCIPtr<T>::makeCopy()
{
if (pointerHolder->isShared())
{
pointerHolder->removeReference();
auto temp = new CLS_CounterHolder;
temp->pointer.reset(new T(*(pointerHolder->pointer)));
pointerHolder = temp;
pointerHolder->addReference();
pointerHolder->markUnshareable();
}
};
这里增加了的间接类为 CLS_CounterHolder。CLS_RCIPtr 通过其间接持有需要引用计数功能的实值。CLS_CounterHolder 指针和实值指针关系是非常紧密的。每当实值对象需要一份新的拷贝时,都需要一个新的 CLS_CounterHolder,反之亦然。因此在 CLS_CounterHolder 类内部,我们使用智能指针保存实值地址。除此之外,我们对模板类也不要其模板参数满足 referencecountable 了。
这里书中的 makeCopy 函数中没有调用最后一句 pointerHolder->markUnshareable();。我对此存疑,因为如果不调用这句话,那么在 operator* 返回应用之后该对象还是可以共享的。这就没有解决返回引用之后再进行拷贝的问题(上面2(4)中的问题)。
(2)CLS_RCIPtr 的使用
class CLS_MyString
{
public:
CLS_MyString(const char* initValue = "");
char& operator[](int index);
private:
CLS_RCIPtr<string> value;
};
CLS_MyString::CLS_MyString(const char* initValue):
value(new string(initValue))
{
}
char& CLS_MyString::operator[](int index)
{
value->operator[](index);
}
这里的 operator[] 指示着两件事:任何我们想要在 CLS_MyString 进行的操作都应被转发到其实值对象 value 中;我们不再需要担心通过 operaotr-> 或 operator[] 访问对象得到指针或引用后,对原对象修改带来的问题了。这个问题已经在代理类中的相应操作符中被处理了。
2、评估
引用计数的实现并非毫无成本。每个拥有此功能的实值都需要携带一个引用计数器,而大部分操作都需要此计数值进行检测或处理。因此,对象的实值需要更多的内存;而且,其操作步骤也更为复杂。此外,引用计数类的底层源代码通常会较为复杂。像我们这里实现的 CLS_MyString 至少需要三个辅助类。我们的复杂涉及在实值可被共享时提供了更加效率的承诺,它消除了追踪对象拥有权的必要性,同时增加了应用技术设计构想的可重用性。
引用计数是一项优化技术,其适用前提是:对象常常共享实值。如果这个假设失败,引用计数反而会配上更多内存,执行更多代码。从另一个角度看,如果你的对象确实有共同实值的倾向,引用计数应该可以帮助你节省时间和空间。对象越大,共享的个数越多,其节约的空间就越多。我们在对象的赋值和复制操作所执行的动作越复杂,其节省的时间就越多。简单地说,以下是使用引用计数改善效率的最佳时机:
相对多数的对象共享相对少量的实值。
对象实值的产生或销毁成本很高,或是它们使用许多内存。
确定是否我们的程序能够受益于引用计数最佳方式就是分析程序。我们可以知道程序性能的瓶颈是否为过多的对象产生和销毁;我们同样可以了解对象个数和其实值个数之比,从而决定到底是否需要引用计数功能。
最后,我们讨论下当引用计数个数减为0时的 delete this。这要求该内存分配与堆上。这一次我们以规范化来达到此目标。CLS_RCObject 是作为需要引用计数功能的实值对象的基类,而这些实值对象只应该被智能指针所管理。因此,只有应用对象(CLS_String 对象)才能将实值对象初始化。因此确保实值对象分配与堆上,是应用对象作者的责任。这就是使用引用计数技术的附加条件:对象诞生规则必须得遵守。
3、同值数据共享(个人补充)
前面提供的类很好的解决了大部分实值数据相同的共享情况,然而还有一种情况我们无法解决重复的实值对象:分开构造,但是拥有相同实值的对象。如果我们想解决此问题,首先想到我们需要一个容器存储某个类已经创建的所有实值。那么这个容器应该保存在哪个类里呢?显然只有智能指针类适合保存此容器对象。这个容器对象应该在对应于某个类型的智能指针对象间共享,因此其属性应该为静态。我们考虑在 CLS_RCPtr 中尝试实现此功能。
(1)CLS_ValueCheck
我们不能选择继承 CLS_RCPtr,因为那样我们无法保证在指针被传递到 CLS_RCPtr 构造函数之前完成唯一性确认。考虑到同值数据共享其实扔是通过 CLS_RCPtr 管理的一种资源,因此可以将其实现为一个类并作为 CLS_RCPtr 的一个模板参数。为了使得不需要同值数据共享的类能仅使用 CLS_RCPtr 引用计数部分的功能,我们需要使用表达式参数加以区分。我这里实现了一个版本的 CLS_ValueCheck,用来监测共享实值对象指针容器。
template<referencecountable T, bool UseOnceValue, typename Container = vector<T*>>
class CLS_ValueCheck
{
public:
static T* checkValue(T* ptr)
{
auto iter = find_if(container.begin(), container.end(), [ptr](const T* element) {return *element == *ptr; });
if (iter == container.end())
{
ptr->addReference();
container.push_back(ptr);
return ptr;
}
// 查找的数据已经在容器中,则不需要使用新生成的ptr
if (*iter != ptr)
{
delete ptr;
}
return *iter;
}
static void removeValue(T* ptr)
{
auto iter = find_if(container.begin(), container.end(), [ptr](const T* element) {return *element == *ptr; });
if (iter != container.end())
{
// 如果数据已经不再被外部共享,则删除
if (!(*iter)->isShared())
{
(*iter)->removeReference();
container.erase(iter);
}
}
}
private:
static Container container;
};
template<referencecountable T, bool UseOnceValue, typename Container>
Container CLS_ValueCheck<T, UseOnceValue, Container>::container;
template<referencecountable T>
class CLS_ValueCheck<T, false, vector<T*>>
{
public:
static T* checkValue(T* ptr)
{
return ptr;
}
static void removeValue(T* ptr)
{
return;
}
};
我们这里使用 find_if 函数隐式要求使用其的类需要实现 operator==(注意此函数需要声明为 CLS_MyString 的友元函数,否则无法访问 StringValue):
bool operator==(const CLS_MyString::StringValue& lhs, const CLS_MyString::StringValue& rhs)
{
return strlen(lhs.pcData) == strlen(rhs.pcData) && strcmp(lhs.pcData, rhs.pcData) == 0;
}
(2)CLS_RCPtr 修改
对于使用它的 CLS_RCPtr 来说,每个函数的定义和类声明都需要增加模板参数。除此之外,每次初始化都需要去 CLS_ValueCheck 中获取唯一的实值对象;每次移除引用时,都需要到 CLS_ValueCheck 中检查唯一实值是否还有人引用:
template<referencecountable T, bool useOnceValue = false>
class CLS_RCPtr;
template<referencecountable T, bool useOnceValue>
class CLS_RCPtr
{
public:
CLS_RCPtr(T* _ptr = 0);
CLS_RCPtr(const CLS_RCPtr& other);
~CLS_RCPtr();
CLS_RCPtr& operator=(const CLS_RCPtr& other);
T* operator->() const;
T& operator*() const;
private:
T* pointer;
void init();
};
template<referencecountable T, bool useOnceValue>
CLS_RCPtr<T, useOnceValue>::CLS_RCPtr(T* _ptr) :
pointer(_ptr)
{
init();
}
template<referencecountable T, bool useOnceValue>
CLS_RCPtr<T, useOnceValue>::CLS_RCPtr(const CLS_RCPtr& other) :
pointer(other.pointer)
{
init();
}
template<referencecountable T, bool useOnceValue>
CLS_RCPtr<T, useOnceValue>::~CLS_RCPtr()
{
if (pointer)
{
pointer->removeReference();
CLS_ValueCheck<T, useOnceValue>::removeValue(pointer);
}
}
template<referencecountable T, bool useOnceValue>
CLS_RCPtr<T, useOnceValue>& CLS_RCPtr<T, useOnceValue>::operator=(const CLS_RCPtr& other)
{
if (pointer != other.pointer)
{
if (pointer)
{
pointer->removeReference();
CLS_ValueCheck<T, useOnceValue>::removeValue(pointer);
}
pointer = other.pointer;
init();
}
return *this;
}
template<referencecountable T, bool useOnceValue>
T* CLS_RCPtr<T, useOnceValue>::operator->() const
{
return pointer;
}
template<referencecountable T, bool useOnceValue>
T& CLS_RCPtr<T, useOnceValue>::operator*() const
{
return *pointer;
}
template<referencecountable T, bool useOnceValue>
void CLS_RCPtr<T, useOnceValue>::init()
{
if (pointer == nullptr)
{
return;
}
if (pointer->isShareable() == false)
{
pointer = new T(*pointer);
}
pointer = CLS_ValueCheck<T, useOnceValue>::checkValue(pointer);
pointer->addReference();
}
为了使用户默认能够使用不共享唯一实值的功能,我们在最开始初始化了一个 useOnceValue 为 false 的 CLS_RCPtr 部分特化版本。在
在实现此功能时,我开始犯了一个错误。在 init 函数中,我本来是这样实现的:
if (pointer->isShareable() == false)
{
pointer = new T(*pointer);
}
else
{
pointer = CLS_ValueCheck<T, useOnceValue>::checkValue(pointer);
}
后来我发现源对象的共享是否使能不应该影响目标对象是否需要被添加对容器中。除此之外,我们可以选择是否保存那些没有共享功能的实值对象指针到 CLS_ValueCheck 的容器中。其结果没有什么区别。
最后还有一部分功能针对那些不支持共享的对象。我们需要像 CLS_RCPtr 一样重载 non-const opeartor->/*。因为我们这个功能的实现依赖于 CLS_RCPtr 对于内部实值指针的完全代理,对其状态能够完全监控。这样我们才能在 cop 时及时更新 CLS_ValueCheck。主要修改位于 makeCopy 函数中:
template<referencecountable T, bool useOnceValue>
void CLS_RCPtr<T, useOnceValue>::makeCopy()
{
auto temp = new T(*pointer);
temp->addReference();
temp->markUnshareable();
pointer->removeReference();
CLS_ValueCheck<T, useOnceValue>::removeValue(pointer);
pointer = temp;
}
这个函数的实现和原来有些区别。我们不再判断对象是否共享决定是否需要执行一次拷贝,因为容器中默认保存了一份引用,因此我们无法通过是否共享判断当前保存的是否为其实值的唯一一份引用(可以通过在 CLS_RCObject 中增加获取引用计数方法进行优化)。因此,我们也需要使用临时对象先保存拷贝后的数据,将原对象的引用和在容器中的引用去除,再将临时对象赋值给 poiner。我们这里选择不保存非共享实值对象指针到容器中。虽然没有尝试,但是我觉得如果改用 shared_ptr 实现类似的功能,CLS_ValueCheck 中应该使用 weak_ptr 保存引用,这样就不会导致需要时刻兼顾容器内部指针的引用问题了。
CLS_MyString 类也需要修改,与前面类似,这里不再列出。
(3)测试
我们这里对同值数据共享和不共享的两种情况分别进行测试。首先在 CLS_RCObject 增加一些日志:
void CLS_RCObject::addReference()
{
++refCount;
cout << "add refCount = " << refCount << endl;
}
void CLS_RCObject::removeReference()
{
--refCount;
cout << "remove refCount = " << refCount << endl;
if (refCount == 0)
{
delete this;
}
}
测试代码如下:
void test()
{
cout << "before construct string1" << endl;
CLS_MyString string1("test");
cout << "after construct string1" << endl << endl;
cout << "before construct string2" << endl;
CLS_MyString string2("test1");
cout << "after construct string2" << endl << endl;
cout << "before construct string3" << endl;
CLS_MyString string3("test");
cout << "after construct string3" << endl << endl;
cout << "before construct string4" << endl;
CLS_MyString string4(string3);
cout << "after construct string4" << endl << endl;
cout << "before calling operator[]" << endl;
string3[0] = 't'; // cow,但是实值数据值不变
cout << "after calling operator[]" << endl << endl;
cout << "before construct string5" << endl;
CLS_MyString string5(string3);
cout << "after construct string5" << endl << endl;
cout << "before construct string5" << endl;
CLS_MyString string6("test");
cout << "after construct string6" << endl << endl;
}
不共享:
同值共享: