C++ 11 新特性总结(四):enable_from_this/shared_from_this
问题起源
问题来自std::shared_ptr
,为了保证shared_ptr
的计数正确,只允许使用一个智能指针对象去使用裸指针构造,因此这样的代码是错误的:
1
2
3Person* p = new Person();
std::shared_ptr<Person> p1(p);
std::shared_ptr<Person> p2(p);1
2
3Person constructor called!
Person destructor called!
Person destructor called!
显式上我们不会这么干,但是有一些隐晦的写法还是触碰到这条红线,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using namespace std;
class Person{
public:
Person(){
cout << "Person constructor called!" <<endl;
}
~Person(){
cout << "Person destructor called!" <<endl;
}
std::shared_ptr<Person> getPersonSharedPtr(){ //通过this返回一个共享指针
return std::shared_ptr<Person>(this);
}
};
int main(){
std::shared_ptr<Person> p = std::make_shared<Person>();
std::shared_ptr<Person> p1 = p->getPersonSharedPtr();
return 0;
}
这里错误的原理和上述是完全一样的,p完成了构造和管理,但是p1通过p的裸指针完成了管理,同样会产生两个引用数为1的控制块,造成double delete。
对于本例,当然直接使p1 = p
能实现正确的引用计数,但是从this获取共享指针对象还是一种美好的夙愿,例如对于插件注册系统,我们不总是可以从当前类中直接获取目标类对象,但是通过接口层可以将指针层层返回,底层所做的极其可能只是提供一个this指针;再者,在boost::asio
异步回调中,良好的shared_ptr管理延长其生命周期,确保异步执行的安全性。这个问题在C++
11得到解决。
std::enable_shared_from_this
与 shared_from_this
std::enable_shared_from_this<T>
是一个类模板,要实现安全地从this获取共享指针,我们要做的就是继承这个类,shared_from_this
在std::enable_shared_from_this
类中一个protected函数,这个类能解决双重删除的关键在于,当我们第一次使用std::shared_ptr
管理裸指针时,enable_shared_from_this
类内部的weak_ptr
成员会记录这个共享指针(实际上就是其控制块),调用shared_from_this的结果就是return weak_ptr.lock()
,新的共享指针不会创建独立的控制块,而是仍然沿用原来共享指针的控制块,解决了双重删除的问题,一个基本使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using namespace std;
class Person : public std::enable_shared_from_this<Person>{ //继承
public:
Person(){
cout << "Person constructor called!" <<endl;
}
~Person(){
cout << "Person destructor called!" <<endl;
}
std::shared_ptr<Person> getPersonSharedPtr(){
return shared_from_this(); //调用父类的函数获得共享指针对象,这个对象是安全的
}
};
int main(){
std::shared_ptr<Person> p = std::make_shared<Person>();
std::shared_ptr<Person> p1 = p->getPersonSharedPtr(); //ok
return 0;
}
异步对象生命周期
在阅读一些来自boost::asio代码会发现异步任务中使用了shared_from_this
的用法,主要是在异步函数中预先占用一个引用数,使得异步任务被调用前类对象不被析构,详见,用法摘要:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void doWork() {
auto self(shared_from_this());
asio::post(timer_.get_executor(), [this]() {
for (int i = 0; i < 10; ++i) {
std::cout << "i=" << i << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // Simulate work
if (timer_.expiry() <= asio::steady_timer::clock_type::now()) {
std::cout << "Work interrupted due to timeout" << std::endl;
return;
}
}
timer_.cancel(); // Cancel the timer if work completes
std::cout << "Work completed without timeout" << std::endl;
});
}
weak_from_this
C++
17引入了weak_from_this
,因为有shared_from_this
获取共享指针,也一定需要weak_from_this
来解决循环引用问题,当然在C++
17以前,可以使用共享指针隐形构造弱智能指针: 1
2
3std::weak_ptr<Node> Node::getWeak(){
return shared_from_this();
}weak_from_this
的妙用,顺便在此记录。
类的双向引用设计
一个通信类和业务类需要互相发送信息,例如rpc发送信息到通信类,调用相应的业务接口,需要实现通信类调用业务类接口;而业务类需要数据时,也需要调用通信类接口来获取数据,需要实现业务类调用通信接口,这是一种双向引用情景。
这里提供了若干种设计思路。
回调函数实现
通信类Node
的函数可以在初始化时进行回调注册,将自己的成员函数注册到业务类Person
的回调函数,业务类使用时只需要调用成员回调函数指针即可。而且业务类使用静态对象向通信提供接口,达到通信类和业务类解耦目的。
1 | ///test.h |
1 | ///test.cpp |
循环引用法
当然这里不是真的要实现循环引用,在现代C++中完全可以实现A类作为B类成员和B类作为A类成员,只要其中一个使用weak_ptr
指针管理即不会造成循环引用问题,这种方法耦合度较高,但只需要声明类成员,无需绑定大量的回调函数指针,在类关系简单、函数量大时可以考虑使用这种方法,而且注意:构造share_ptr
管理的那个类时,需要先完成自身构造、再去构造用weak_ptr
构造的那个类(毕竟习惯上我们先构造所有成员类再构造自身)。
而且通过C++ 17引入的weak_from_this能够简单地引入弱指针示例,以下:
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
27
28
29
30
31///test.h
class Person;
class Node : public std::enable_shared_from_this<Node>{ //通信节点类
public:
Node();
~Node();
void init();
void doCount();
int getNum(int startNum);
private:
std::unique_ptr<Person> mPersonPtr;
};
using GetNumFunc = std::function<int(int)>;
class Person{ //业务类
public:
Person(std::weak_ptr<Node> node);
int doPersonCount();
private:
std::weak_ptr<Node> mInterfaceNode;
};
1 | ///test.cpp |
weak_ptr
的拷贝开销很小,用拷贝语义基本没什么问题。
参考链接: