前几天为了查找一个BUG花了几乎一整天的时间,然后——就像江湖传说的一样,只改了一行代码就搞定了。至于原因仅仅是因为一个变量未初始化,一秒钟的疏忽却花了一天的代价来弥补,就像那个蚂蚁和大象的段子:风流了一夜,挖一辈子的坑。
这类BUG通常是不稳定重现的测试未必测的出来,测出来了也未必容易定位,若是不小心留到了用户那里可能真的就成了一辈子的坑,永远都解决不掉了。这样的事我想命苦的C++程序员们初入江湖的时候可能都遇到过那么一两回。——声明一下,以前我也搞出过这样的BUG,但从那之后没再犯第二次错误,这次的代码是别人写的。
俗话说人是靠不住的,只有靠制度才有保障,哪怕我们再小心也总有马虎大意的时候。所以今天我们想讨论的就是怎样以“制度”的方式一劳永逸的解决这个问题。方案其实挺简单,就是把成员变量封装到一个独立的类里,在构造函数里做初始化,然后用这个类的对象替代普通的成员变量。
这里我们一步到位直接给出一个模版实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | template < typename T, T _v> class Property { public : Property( void ) : m_value(_v) { } operator T( void ) const { return m_value; } T operator=(T v_) { m_value = v_; return m_value; } private : T m_value; }; |
为什么取名叫Property我们后面再说,先看使用方式:
1 2 3 4 5 6 7 8 9 10 11 12 | class Rectangle { public : int GetWidth( void ) const { return m_width; } int GetHeight( void ) const { return m_height; } void SetWidth( int width) { m_width = width; } void SetHeight( int height) { m_height = height; } private : Property< int , 0> m_width; Property< int , 0> m_height; }; |
一个小小的模版不仅解决了可能忘记初始化成员变量的问题,而且买一送一,直接在声明变量的时候就可以指定初始化值,避免初始化代码和变量声明代码两地分居,很强大吧。
从小就有人教导我们说成员变量要声明成private的,然后写一组get/set函数来做读写操作。上面的Rectangle类中我们就是这么做的,但是事实上我们已经用Property模版对真正的成员变量的访问封装过一层了,再封装一层get/set函数显得有些多余,完全可以简化一下,简化版本的Rectangle如下:
1 2 3 4 5 6 7 | class Rectangle { public : Property< int , 0> width; Property< int , 0> height; }; 使用起来也更方便直观: |
1 2 3 4 5 6 | Rectangle rect; rect.width = 320; rect.height = 240; int width = rect.width; int height = rect.height; |
如果你用C++之余还使用过C#之类的语言那么你一定对其中的property(属性)印象深刻,并且叹息C++中为什么没有这样的好东西。不过C++有一个优点就是几乎什么东西都可以自己做,我们的Property模版就相当于为C++添加了一个简化版的“属性”,当然我们还需要继续重载大于小于等操作符进一步完善它。
美中不足的是我们虽然通过Property封装了对成员变量的读写操作,但没有实现对读写操作的监控,实际上设计get/set函数特别是set函数最重要的目的就在于此。我这个人比较喜欢花哨的语法糖,所以下面我们就用C++中最摩登的方法实现对成员变量写操作的监控,方案是给Property增加一个function用于事件回调。代码如下:
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 32 33 | template < typename T, T _v> class Property { public : Property( void ) : m_value(_v) { } operator T( void ) const { return m_value; } T operator=(T v_) { if (m_value != v_) { m_value = v_; if (OnChanged != nullptr ) { OnChanged(); } } return m_value; } public : typedef std::function< void ( void )> _Event; _Event OnChanged; private : T m_value; }; |
使用时只要给OnChanged成员赋值就可以了。下面的代码中使用了lambda表达式赋值,可以在lambda表达式中进行事件处理:
1 | rect.width.OnChanged = []{ /* do something */ }; |
更理想的方案是实现事件多播,也就是为OnChanged绑定多个function对象,这样就可以像C#中的事件那样使用:
1 2 | rect.width.OnChanged += []{ /* do something */ }; rect.width.OnChanged += []{ /* do another thing */ }; |
由于C++的function本身不支持多播,所以必须自己实现。简单的方法是使用一个vector存储function对象,不过使用起来有点麻烦,更好的方法是自己实现一个支持多播的Event类,这个实现起来有点难度我还没尝试过,如果你做过类似的东西或者有更好的实现多播的方法可以交流一下。
关于function和lambda表达式的使用可以参考我之前的两篇文章:《使用function改进设计》和《在VS2010中使用auto关键字和lambda表达式》。
本文地址:http://www.cnblogs.com/xrunning/archive/2011/11/10/2243826.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux glibc自带哈希表的用例及性能测试
· 深入理解 Mybatis 分库分表执行原理
· 如何打造一个高并发系统?
· .NET Core GC压缩(compact_phase)底层原理浅谈
· 现代计算机视觉入门之:什么是图片特征编码
· 手把手教你在本地部署DeepSeek R1,搭建web-ui ,建议收藏!
· Spring AI + Ollama 实现 deepseek-r1 的API服务和调用
· 数据库服务器 SQL Server 版本升级公告
· 程序员常用高效实用工具推荐,办公效率提升利器!
· C#/.NET/.NET Core技术前沿周刊 | 第 23 期(2025年1.20-1.26)