0%

Use case for smart pointer

看了書之後豁然開朗啊,之前還真是個半調子。總結一下使用情境。

從失敗的例子講起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Obj {
string name_;
Obj(const string& name) :name(name_) {}
~Obj() {}
};
class ObjFactory {
map<string, shared_ptr<Obj>> lookup_;
public:
shared_ptr<Obj> get(const string &name)
{
auto it = lookup_.find(name);
if (it != lookup_.end())
return it->second;
lookup_[name] = shared_ptr<Obj>(new Obj(name));
return lookup_[name];
}
};

這段程式碼最大的問題就是,如果ObjFactory的instance沒被摧毀,所有拿到的Obj都不會被釋放。

用 weak_ptr 取代 shared_ptr

在ObjectFactory的部份不要保存shared_ptr,這樣會增加reference count,用weak_ptr取而代之。需要的話再promotion成shared_ptr。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ObjFactory {
map<string, weak_ptr<Obj>> lookup_;
public:
shared_ptr<Obj> get(const string &name)
{
auto it = lookup_.find(name);
if (it != lookup_.end())
{
shared_ptr<Obj> obj((it->second).lock());
return obj;
}
shared_ptr<Obj> instance(new Obj(name));
weak_ptr<Obj> obj(instance);
lookup_[name] = obj;
return instance;
}
};

看起來沒什麼問題,不過譽到以下這種情況就爛了

1
2
3
4
5
6
7
8
9
10
for (int i = 0; i < 3; i++)
{
shared_ptr<Obj> s = pFactory->get("HM");
if (s) {
cout << "create new obj" << endl;
}
else {
cout << "cannot create new obj" << endl;
}
}

我們希望看到的是每次都能拿到一個新物件,結果發現只有第一次能成功。原因出在當Obj被摧毀的時候,沒有順便清理掉ObjFactory 當中的lookup_的資料,以致於下一次使用的時候,可以找到上一次殘留的屍體,promtion之後就是一個空的shared_ptr。
解決方法就是使用shared_ptr時,同時自訂一個destructor,除了釋放memory之外,也把map裡面的資料輕空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ObjFactory {
map<string, weak_ptr<Obj>> lookup_;
void deleteObj(Obj *pObj)
{
lookup_.erase(pObj->name_);
delete pObj;
}
public:
shared_ptr<Obj> get(const string &name)
{
auto it = lookup_.find(name);
if (it != lookup_.end())
{
shared_ptr<Obj> obj((it->second).lock());
return obj;
}
shared_ptr<Obj> instance(new Obj(name), bind(&ObjFactory::deleteObj, this, placeholders::_1));
weak_ptr<Obj> obj(instance);
lookup_[name] = obj;
return instance;
}
};

現在看起來正常了,不過街下來這個Case又會遇到問題。

1
2
3
4
{
shared_ptr<Obj> obj1 = pFactory->get("Obj1");
delete pFactory;
}

由於在這個block之內,pFactory已經被釋放了,所以那個destructor的this一點都不可靠,因此要把裡面那個this轉成一個shared_ptr。

enable_shared_from_this

enable_shared_from_this就是因此登場的,他可以把this指標所在的位置轉成一個shared_ptr。
而原先的測試部份也必須用shared_ptr管理了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ObjFactory : public enable_shared_from_this<ObjFactory> {
map<string, weak_ptr<Obj>> lookup_;
void deleteObj(Obj *pObj)
{
lookup_.erase(pObj->name_);
delete pObj;
}
public:
shared_ptr<Obj> get(const string &name)
{
auto it = lookup_.find(name);
if (it != lookup_.end())
{
shared_ptr<Obj> obj((it->second).lock());
return obj;
}
shared_ptr<Obj> instance(new Obj(name), bind(&ObjFactory::deleteObj, shared_from_this(), placeholders::_1));
weak_ptr<Obj> obj(instance);
lookup_[name] = obj;
return instance;
}
};
{
shared_ptr<ObjFactory> pFactory(new ObjFactory);
shared_ptr<Obj> obj1 = pFactory->get("Obj1");
}

Misc

使用shared_ptr會延長ObjectFactory的LifeCycle,如果pFactory已經不在的話,跟本連清理的動作都不用作。
所以程式可以寫成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ObjFactory : public enable_shared_from_this<ObjFactory> {
map<string, weak_ptr<Obj>> lookup_;
static void deleteObj(const weak_ptr<ObjFactory> &pWeakFactory, Obj *pObj)
{
shared_ptr<ObjFactory> pFactory(pWeakFactory.lock());
if (pFactory) {
pFactory->lookup_.erase(pObj->name_);
}
delete pObj;
}
public:
shared_ptr<Obj> get(const string &name)
{
auto it = lookup_.find(name);
if (it != lookup_.end())
{
shared_ptr<Obj> obj((it->second).lock());
return obj;
}
shared_ptr<Obj> instance(new Obj(name), bind(&ObjFactory::deleteObj,
weak_ptr<ObjFactory>(shared_from_this()), placeholders::_1));
weak_ptr<Obj> obj(instance);
lookup_[name] = obj;
return instance;
}
};

結論

有本好書真的很重要啊,以前我只會用最粗淺的shared_ptr,對於wear_ptr跟其他特性玩全部熟。有了範例之後至少有個基本認識。
至於程式寫得這麼複雜到令人髮指也是不太好,Garbage collection很大部份可以紓解Programmer的負擔。