前几天为了查找一个BUG花了几乎一整天的时间,然后——就像江湖传说的一样,只改了一行代码就搞定了。至于原因仅仅是因为一个变量未初始化,一秒钟的疏忽却花了一天的代价来弥补,就像那个蚂蚁和大象的段子:风流了一夜,挖一辈子的坑。
这类BUG通常是不稳定重现的测试未必测的出来,测出来了也未必容易定位,若是不小心留到了用户那里可能真的就成了一辈子的坑,永远都解决不掉了。这样的事我想命苦的C++程序员们初入江湖的时候可能都遇到过那么一两回。——声明一下,以前我也搞出过这样的BUG,但从那之后没再犯第二次错误,这次的代码是别人写的。
俗话说人是靠不住的,只有靠制度才有保障,哪怕我们再小心也总有马虎大意的时候。所以今天我们想讨论的就是怎样以“制度”的方式一劳永逸的解决这个问题。方案其实挺简单,就是把成员变量封装到一个独立的类里,在构造函数里做初始化,然后用这个类的对象替代普通的成员变量。
这里我们一步到位直接给出一个模版实现:
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我们后面再说,先看使用方式:
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如下:
class Rectangle { public: Property<int, 0> width; Property<int, 0> height; }; 使用起来也更方便直观:
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用于事件回调。代码如下:
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表达式中进行事件处理:
rect.width.OnChanged = []{ /* do something */ };
更理想的方案是实现事件多播,也就是为OnChanged绑定多个function对象,这样就可以像C#中的事件那样使用:
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