加载中...
C++编程要避免使用单例模式吗?
第1节:代码改善:一个“坑爹”的文字类冒险游戏
第2节:在禁止多重继承的情况下,如何设计“直立智慧猩猩”类?
第3节:C++多线程代码中的“乱序”执行现象
第4节:C++中函数指针有什么作用呢?
第5节:为什么我用c++写的游戏那么简陋?
第6节:多线程读写socket导致的数据混乱的原因是什么?
第7节:WebSocket 是什么原理?为什么可以实现持久连接?
第8节:怎样在c++中实现instanceof?
第9节:一个函数多处 return 是好风格吗?
第10节:C++中虚函数相比非虚函数的优势
第11节:为什么 C::C::C::C::foo() 能编译成功?
第12节:如何静态反射C++枚举的名字
第13节:看C++大叔如何拥 java 妹子入怀……
第14节:坨——理解递归实现“汉诺塔”代码的关键
第15节:C++编译器如何实现 const(常量)?
第16节:C++如何为断言加上消息
第17节:初学C++到什么水平,算是合格的初级开发工程师?
第18节:C++编程要避免使用单例模式吗?
第19节:学习C++要学boost库吗?
第20节:C++的继承就是复制吗?
第21节:C++构造函数失败,如何中止创建对象?
第22节:C++学完多线程后,学什么呢?
第23节:string_view 适合用做函数的返回值类型吗?
第24节:为指针取别名,为何影响const属性?
第25节:std::enable_shared_from_this 的存在意义?
课文封面
  1. 现实业务是不是单例,高于一切其它原则
  2. 单例是最好的全局变量
  3. 只读的单例是最好的单例
  4. 有单例,不意味着一定要有单例依赖

0. 问题

《C++ Core Guidelines 解析》已经有中文在售版本,是一本非常棒的C++的技术书。我强烈推荐大家购买学习(我自己第一时间买,已经读完第一遍)。

不过,作为“指南”式的书,并且是以“条款”形式展现,那么,就无法完全避免像“人生指南”一样,有些条款几乎是公理,但有些条款却无法处处适用;特别是 C++ 这样一门适用面非常广的语言。

“避免单例” 这个条款,就让很多读者无法理解及接受。

首先,单例模式 是著名的《设计模式》一书中22个模式之一。并且算是众模式中,相对比较好理解的模式之一,很多程序员,包括C++程序员都学习了,并且应用了,现在突然这边出了核心指南,说要避免单例了?

看完下面的南老师的回答,或许能帮助到你。

1. 对话

甲:C++不提倡的使用单例模式的话,C++标准库里的 cout、cin、clog、cerr 这几个老家伙应该先废掉。

乙:可是我给初学者的第一个"Hello World!" 例程,就得用到 cout 。没有现成的,别说初学者,一些 C++“高手”也不懂如何手工写代码正确地创建出一个正确的cout 对象。

甲:没关系,在你指出 avoid 掉单例模式之后,你想想普通程序员应该怎么使用那些全局确实只有一份,却几乎处处要使用的数据?他们会怎么解决,你就怎么解决。

所以,我们有了新的C++主函数原型:

int main (std::istream& cin, std::ostream& cout, std::ostream& cerr, std::ostream& clog, int argc, char* argv[] );

光这样其实还不够呢,一个程序同时使用 cin 和 wcin;cout 和 wcout 的情况也不少见,所以,下面这个版本更成熟:

int main (std::istream& cin, std::ostream& cout, std::ostream& cerr, std::ostream& clog, std::wistream& wcin, std::wostream& wcout, std::wostream& cerr, std::wsostream& wclog, int argc, char* argv[] );

嗯,我们是不是要开始考虑,让 main 支持更多重载……想想所有C++入门书籍上来就得改第一课,也算为全球经济恢复与提升做了贡献了。

乙:等等,我这个 cout 就是个全局变量,你从哪看出我用了 单例模式了?
甲:全局变量?那它在定义时, 加 const 限制了吗?
乙:人家一个单纯的cout,职责就是要存储屏幕待输出内容的数据,你让它怎么加 const 变成常量嘛?
甲:我倒不敢这么说,只是书里有个条款 “I.2 避免非 const 全局变量” 。
乙:这个,总会有特殊情况嘛!
甲: I.2 的“避免非 const 全局变量” 和 I.3 的“避免单例”,一前一后,但是它们的“避免”却各有各的含义?
乙:怎么可能?

甲:因为有人说,“避免单例”,就是指程序中一个单例都不能要?
乙:反正书里没这么说,可能有些读者是原教旨派的吧,总喜欢搞极端,讨厌!不过,我们还是来继续说单例吧?我不喜欢说全局变量的事……
甲:C++是比较容易出原教旨派用户的语言吗?
乙:我刚说的原教旨派,不是指C++的原教旨派,而是在说英语的原教旨派,就是那些看到一个 avoid 单词就兴奋地想到女生洗澡的 C++用户。TM 的我们为什么跳到这种话题?说单例!
甲:可是,书里明确说过:“单例就是全局变量”。
乙:可是,书里可没说“全局变量就是单例”呀,哈哈哈哈 :)。
甲:如果有一种类型的对象,不允许用户自己随便创建它……
乙:嗯。
甲:并且,也不允许用户复制它,典型的,就是把所在类的拷贝构造等函数,标识成 deleted function……
乙:嗯。
甲:再并且,还非常在意这个全局变量在并发环境下的使用,用了不少手段以确保该全局变量的构建次序(时机),避免因并发而重复初始化, 或者因并发而未初始化就被使用……
乙:嗯??这不就是单例吗?我虽然反对,但是我的反对是建立在我对它非常之了解的基础上的,我要你教 !
甲:cout 允许用户自行创建吗?
乙:当然不行!它们本来就只有一个!
甲:cout可以被值复制吗?
乙:当然不行!复制它们在业务上有什么意义吗?我就知道总有刁民想害朕!朕已经把它的类的拷贝构造,标识为 deleted function 了。身为标准库,我们在特性应用上,历来主张“dogfooding”,所以在c++11之后,STL就用上了 delete ……

有图有证据:

尝试复制cout对象

甲:那你有没有? 以及如果有,那么是采用了哪些手段以确保 cout 等对象在创建和使用方面,不会出现并发冲突?

乙:那是必然的啊!得益于C++强大的设计,一个C++程序是完全可以在 进入 main () 函数之前就跑起来干活的,实际上是:可以在main()函数之前就创建多个线程干活;但另一方面,在 main() 函数之前, cout 可能都还没有构建好,怎么解决这个问题呢?这里就不得不提一下我们的精妙设计了。我们先是 在 ostream 祖上的基类 std::io_base 里,添加并对外开放(public)一个嵌套工具类,它叫 class Init , 有了这个 Init 类,它能被“ used to ensure that the construction of the standard I/O stream objects occurs ……”

甲:等等,等等,您不要激动,更不要飚英文。我记得 ,C.41 是这么说的 “构造函数应当创建完全初始化的对象,类不应有 init (初始化)成员函数,不然就是自找麻烦。”所以,一个类有一个 init 方法用于二级构造是自找麻烦,但内置并开放出一个 Init 类,就值得飚英文吗?

乙:你是不是来找事的?你是不是打 Rust 那边过来的!!!我这叫自然麻烦吗? 是事情的本身就这么麻烦!!!“复杂的事情复杂做,简单的事情简单做”,这是我们C++世界的基本原则之一,是排在“零抽象原则”_之前的原则……

甲:我才不是Rust的拥趸;再说,我还写了一本《白话C++》呢,里面保证很认真的讲解了 “复杂的事情复杂做,简单的事情简单做”等原则。

乙:好你个臭卖书的,竟然敢借机做软广!

3. 结论

其实,单例不应该随随便便使用,显然值得认同,而最应该认同的,是对话中的那句 “我这叫自然麻烦吗? 是事情的本身就这么麻烦”。 事情,也就是 “business”。事情原本的逻辑以及适合的表在是怎样的,才是第一位。请读者一定要记住: 软件行业中的设计 ,Core Guidelines 的第零条,永远是 “业务实际”是第一设计要素。这里的业务实际,包括但不限于:

  1. 业务逻辑是怎样的?你的客户做电商,但他们的水平看起来就是十年内做不出太阳系,那么,假设程序中此时确实需要有个 “太阳历”的对象,它就是单例。不要去想:有一天,万一客户的销售范围是全宇宙,而宇宙里有两个太阳,那我这个程序该怎么办啊?单元测试就直接通不过了嘛?? (请换个思维:到时要么是我们借机逼让客户加钱成功,要么就是客户钱多,换更好的团队了)
  2. 开发团队当下的能力与水平怎么样?比如客户确实现在就充满了做到全宇宙的王者之风,但我们的能力确实没办法事事考虑,现在就设计出一套通行全宇宙的系统。能怎么办?有十分能力的团队,可以接客户嘴里的12分的业务场景,但实际出于负责任,你应该拿客户10分的钱,让你的技术团队卡在8分的位置做最可靠的开发。剩下的4分,靠的是技术之外的能力。否则公司养其他团队,比如售前,销售干嘛?最可怕的事就是销售接了12分的业务,让技术团队吊在14分的水平开发,然后只要了 8分的钱……

一个对象应不应当设计以及实现为单例,请分析它是不是真的就是一个单例(再次提醒:不能光是纯逻辑,也包括来自外界,比如客户和研发团队自身的特点),如果是,就是,如果不是,就不是。

以 cout 为例, 我们干涉不了它已经是一个单例对象的,但是我们可以减少以单例的形式,使用单例对象,比如,下面的两个函数,第一个依赖单例对象,第二个没有依赖:

代码一:依赖于 cout 单例

// 依赖单例 void Hello (std::string const& name) { cout << "你好!" << name; }

代码二:不依赖单例

// 不依赖单例 void Hello (std::ostream& os, std::string const& name) { os << "你好!" << name; }

如果可以给建议,我觉得条款写成 “减少对单例的依赖”,或者,哪怕是“避免单例的使用”,都 比较严谨。 而 “避免单例”……不,单例是客观存在,避免不了。

说一下我的条款:

1. 现实业务是不是单例,高于一切其它原则;说明见上。
2. 单例是最好的全局变量。(创建时机可靠,需要调试时,有 class 所拥有的一切 用户自定义的机会)
3. 只读的单例是最好的单例:如果现实业务中已经是单例,而且它还满足只读(典型的如全局只读配置,即只在程序启动时从外部加载数据进来,从此就不再修改),那谁不用单例,谁就是在自找麻烦。
4. 存在单例,不意味着必然有单例依赖。比如,很多配置都是典型的“树”,然后,当你的某个方法,只是需要用到这颗树上的某个分支,甚至就是某个页子节点。应该把这个配置项,作为方法的一个有明确字面含义的入参。这也符合封装的原则(不要只为了一片叶子,就要求对方开放整棵树)。

顺带的,如果就不管什么原因,反正单例模式要被封杀(可以看了不该看的演出?),那工厂模式也几乎可以判死刑了,好多工厂 模式也天生(业务 逻辑与使用方便性上),就是个单例。