当前位置: 首页 > news >正文

结合C++智能指针聊聊观察者模式

0. 问题

问题是这样,三个类A,B,C。AC都有指针指向同一个B类对象,C类可以回收了刚刚生成的B类对象的内存,A类应该对这个指针进行如何操作,才能确保使用该指针时不会产生野指针问题发生未定义结果?

这是前两天面试的时候面试官问我的问题,当时忘了询问解决方案,我当时的回答时使用智能指针或者进行内存分配管理避免这种情况的发生,但是面试官要求情况就是这样不用智能指针单纯这样情形下如何如何解决,我在网上也没有找到类似的解决方案,所以来请教一下各位大佬这种情况应当如何解决。

 题外话

我一直觉得 std::shared_ptr 得有一个 单线程 特化版本。

本来, std::weak_ptr 和 std::shared_ptr 天然构造 了一对观察者和被观察者,可是,为了支持并发安全,std::shared_ptr 带上了锁,性能代价一下子变大,于是,C++程序员只能闲着就自己搞观察者模式了……

我记得  boost.asio 就有类似的设计:为 io_context (以前的 io_service) 单线程运行特化一个性能版本:

explicit io_context(int concurrency_hint); // 为1时,内部凡需锁操作,都会跳过

估计是因为后面才加入的逻辑,所以没有走类型特化。显然,这里用特化,性能更好,并且会更安全(比如:不会允许将单线程版本下的指针和多线程下的指针互相赋值)。

自己依据业务需要实现观察者模式,当然功能更强大,比如“偷窥/观察”的内容可能五花八门——但是,在C++的指针应用中,真的有大量大量大量情况,我们需要“偷窥/观察”的事情,就一件事:

那个对象它死了没?那个对象它死了没??那个对象它死了没???没死我就继续搞,死了?那就这样吧,我放弃。

这种情景需求太常见太常见,以致C++程序员在这种情景下发生的典型错误 ,叫有个术语:“悬挂指针 / Dangling pointer”。

再说一次:本来 shared_ptr/weak_ptr 就可以完美的帮助C++程序员大量的此类工作,可是因为性能原因,不敢用了。

另,std::expiermental 里倒是有一个 observer_ptr<T>, std::experimental::observer_ptr - cppreference.com。它有它的价值,但它只有观察者,是被观察的对象不知情的情况下的观察,所以它更准确的名字 应该叫 “ 偷窥者 / peeper_ptr<T>”。

1. 观察者模式基本概念 (通用设计)

言归正传。A 喜欢“远程”用 B,但 B 的主人是 C;于是 A 每次都得爬 C 家的墙,偷窥一眼确保 B 还是鲜活的,再回家“远程”用 B。

C 从中发现了商机。对 A 说:“别这么费力了,请注册加入我的观察者列表,这样当 B 一死,我就第一时间通知你。会费只需一年五毛。值当不?”

A :“值当!”

2. 针对题意的设计考虑 (业务定制)

C 为什么只收五毛钱的年费呢?因为 C 知道,全城有 5千万个对象,和A 一样的在偷偷观察 B 呢!一年营业额 两千五百万啊!

但是,C 也有它的烦恼:

  1. B 是自由的,万一它自行和全城的人结合怎么办?到时就没人来翻我家墙了……
  2. 同理, new 一个新B 一点不难,万一别人自己去创建B了,我还做什么生意啊?
  3. 记录五千万个观察者,这需要占用我不少资源啊!!身为奸商,我需要减少记录成本!!
  4. B一死,老子我还真得一个个通知过去,发短信要钱,发微信也很费手指啊!!!
  5. 最后,有人一年用了几千次B,有人只用一次,我却收统一的年费,这不符合商业道德。

2.1 夺取惟一生杀大权

前两个问题要一起解决 。

除非有什么特殊情况,否则,通常地,负责生的要负责死,负责死的也要负责生。一句话:私有化 B 的构造和析构,再强行逼 B 声明 C 是友元。从此 只有 C 可以生B和杀B!不怕有人抢生意了!代码如下。注意,如前所述,下面代码只能用在单线程环境下。如是多线程环境,请直接使用 shared_ptr/weak_ptr 。

#include <iostream> 
#include <memory> 
#include <list> struct B 
{ void Answer() { std::cout << "老娘我真是个万人迷!" << std::endl; } 
private: B() {}; // 生:私有化 
~B() {}; // 死:私有化 friend struct C; // 说是朋友,其实…… 
}; struct C 
{ C() : _b(new B) {} // 造B~C() { DestroyB(); } void DestroyB() // 毁B {delete _b;_b = nullptr; for (auto o : _observers) { o->Notify(); } } B* GetB() { return _b; } /// 快来注册"偷窥"俱乐部 // 
// 俱乐部 必须 有一个共同的头衔: BObserver 
// 从而能收到 B 的死亡通知 struct BObserver 
{ void virtual Notify() = 0; }; // 加入俱乐部 void Regist(BObserver* o) { // 简化,这里不去重了 if (o != nullptr) { _observers.push_back(o); } } // 退出俱乐部 void Unregist(BObserver* o) { _observers.remove(o); } private: B* _b; std::list<BObserver *> _observers; 
};struct A : C::BObserver { explicit A(B* b) : _b(b) {} // 自己造不了B,只能外面传入 void AskB() { if (!_b) { std::cout << "啊,亲爱的B,你死了!" << std::endl; return; } std::cout << "亲爱的B,自我介绍一下吧?\n"; _b->Answer(); } void Notify() override { _b = nullptr; // 没别的事,就是B死了 } B* _b = nullptr; 
}; int main() 
{ C c; // 开张啦!全城独家 A a (c.GetB()); // 来了一个客户,叫小a c.Regist(&a);  // 它办VIP卡了! a.AskB(); // 小a用户(第一次)请求消费c.DestroyB(); // 毁 a.AskB(); //小a用户再次请求消费,但此时B已死
}

「在线运行以上代码」

输出示例:

亲爱的B,自我介绍一下吧?
老娘我真是个万人迷!
啊,亲爱的B,你死了!

2.2 简化

奸商的后面三个问题,也可以一起解决 。它们是:

3. 记录五千万个观察者,这需要占用我不少资源啊……
4. B一死,老子我还真得一个个通知过去……
5. 有人一年用了几千次B,有人只用一次,我却收统一的年费……

很明显,在本例中,不管A有几个对象加入俱乐部,也不管将来有多少新的观察者类型,它们都只是想实现B活着用B,B死了放弃这样一个需求而已……

因此,我们大可不必为了观察者而观察者,奸商 C 只要控制了 B 的生死,再控制好别人只能通过它来访问到全城惟一的 B ,不就好了吗?哪里需要 std::list 来存储客户数据呢?又哪里需要在B死了以后去一一通知呢?

应该把资源,花在真正的商业逻辑。

上,所以,应该在客户每访问一次B时,就记录一下它该交的钱又增加了……

所以,砍掉 list成员,砍掉 BObserver 接口,砍掉 Regist() / Unregist(),砍掉Notify(),砍掉virtual/ override ,砍掉派生,砍掉 面向对象,砍掉设计模式……

#include <iostream>
#include <memory>struct B 
{void Answer(){std::cout << "老娘我真是个万人迷!" << std::endl;}private:B() {};~B() {};friend struct C; // 说是朋友,其实……   
};struct C 
{C() : _b(new B) {} // 造B~C() {DestroyB();} void DestroyB(){delete _b;_b = nullptr;}B* GetB() {return _b;}private:B* _b;
};struct A 
{void AskB(C & c){if (auto b = c.GetB(); !b){std::cout << "啊,亲爱的B,你死了!" << std::endl;} else{std::cout << "亲爱的B,自我介绍一下吧?\n";b->Answer(); }}
};int main()
{C c; // 开张啦!全城独家A a; // 来一个客户a.AskB(c); // c收钱啦c.DestroyB(); // 毁a.AskB(c); // c又要收钱啦……
}

3. 贤者模式

现在, 代码行数减半,逻辑也相应简单很多,因此我们可以认真看一下C,此刻的C做了什么?来看它的代码:

struct C 
{C() : _b(new B) {} // (a): 造B~C() {DestroyB();} void DestroyB(){delete _b;_b = nullptr; // (b):  _b 死后,将它置空}B* GetB() // (c): 对外开放 _b{return _b; }private:B* _b;
};

我加了 (a), (b), (c) 三个注释,C 也就做了这三件事。事实上它的用户,在使用B时,每次都主动取指针,然后每次都仍然得自行判断 是否为空。这 std::weak_ptr 对 std::shared_ptr 的观察实现很接近:weak_ptr 并不能直接使用,你得 通过lock()来升级以得到一个shared_ptr,lock() 也不一定成功,一样得判断得到是不是空指针。

  • 我们的例子
if (auto b = c.GetB())
{... // b 不为空时执行
  • weak_ptr 的例子
if (auto sp = w.lock())
{... // sp 不为空时执行

客户写这样的代码,是烦不胜烦的。所以,C 到底提供提供了什么价值呢?我们像贤者一样陷入了新的沉思……

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • WPF 框架 Prism IActiveAware接口使用
  • 前端项目本地的node_modules直接上传到服务器上无法直接使用(node-sasa模块报错)
  • boost::regex_replace的使用
  • 海外ASO:iOS与谷歌优化的相同点和区别
  • LabVIEW电子水泵性能测试平台
  • 四个“一体化”——构建数智融合时代下的一站式大数据平台
  • 印尼Facebook直播网络需要达到什么要求?
  • 微信小程序毕业设计-汽车维修项目管理系统项目开发实战(附源码+论文)
  • 图——图的遍历(DFS与BFS)
  • Go 语言 UUID 库 google/uuid 源码解析:UUID version4 的实现
  • Apache功能配置:访问控制、日志分割; 部署AWStats日志分析工具
  • MySQL 面试真题(带答案)
  • pxe高效网络批量装机
  • 0基础学会在亚马逊云科技AWS上利用SageMaker、PEFT和LoRA高效微调AI大语言模型(含具体教程和代码)
  • 服务客户,保证质量:腾讯云产品的质量实践
  • [nginx文档翻译系列] 控制nginx
  • 2019年如何成为全栈工程师?
  • crontab执行失败的多种原因
  • es的写入过程
  • Python 使用 Tornado 框架实现 WebHook 自动部署 Git 项目
  • Python爬虫--- 1.3 BS4库的解析器
  • python学习笔记 - ThreadLocal
  • Spring技术内幕笔记(2):Spring MVC 与 Web
  • 关于 Cirru Editor 存储格式
  • 看图轻松理解数据结构与算法系列(基于数组的栈)
  • 入门到放弃node系列之Hello Word篇
  • 微信开放平台全网发布【失败】的几点排查方法
  • 小程序上传图片到七牛云(支持多张上传,预览,删除)
  • 携程小程序初体验
  • 正则与JS中的正则
  • ​【原创】基于SSM的酒店预约管理系统(酒店管理系统毕业设计)
  • (1)安装hadoop之虚拟机准备(配置IP与主机名)
  • (MATLAB)第五章-矩阵运算
  • (补)B+树一些思想
  • (差分)胡桃爱原石
  • (非本人原创)史记·柴静列传(r4笔记第65天)
  • (免费分享)基于springboot,vue疗养中心管理系统
  • (免费领源码)Java#ssm#MySQL 创意商城03663-计算机毕业设计项目选题推荐
  • (三) diretfbrc详解
  • (已解决)Bootstrap精美弹出框模态框modal,实现js向modal传递数据
  • (幽默漫画)有个程序员老公,是怎样的体验?
  • (转)Mysql的优化设置
  • (转)全文检索技术学习(三)——Lucene支持中文分词
  • .Net core 6.0 升8.0
  • .NET Core中如何集成RabbitMQ
  • .Net IE10 _doPostBack 未定义
  • .NET 通过系统影子账户实现权限维持
  • .Net高阶异常处理第二篇~~ dump进阶之MiniDumpWriter
  • .Net小白的大学四年,内含面经
  • @cacheable 是否缓存成功_让我们来学习学习SpringCache分布式缓存,为什么用?
  • @for /l %i in (1,1,10) do md %i 批处理自动建立目录
  • @javax.ws.rs Webservice注解
  • @RequestMapping处理请求异常
  • @SuppressWarnings注解
  • [ Linux 长征路第二篇] 基本指令head,tail,date,cal,find,grep,zip,tar,bc,unname