All articles, tagged with “cxx”

C++ Singleton (单例) 模式最优实现

我非常赞成合理的使用 设计模式 能让代码更容易理解和维护, 不过我自己除了简单的 单例 (Singleton) 模式 外, 其它都很少用 :-)

可耻的是, 直到前段时间拜读了 C++ In Theory: The Singleton Pattern, Part I, 我才发现自己的 单例 (Singleton) 模式 写法还有改进空间.

文章作者 J. Nakamura 以 Log 日志类列举了 单例 (Singleton) 模式 的三种写法:

// log.h
#ifndef __LOG_H
#define __LOG_H

#include <list>
#include <string>

class Log {
public:
  virtual void Write(char const *logline);
  virtual bool SaveTo(char const *filename);
private:
  std::list<std::string> m_data;
};
#endif // __LOG_H

静态化并不是单例 (Singleton) 模式

初学者可能会犯的错误, 误以为把所有的成员变量和成员方法都用 static 修饰后, 就是单例模式了:

class Log {
public:
  static void Write(char const *logline);
  static bool SaveTo(char const *filename);
private:
  static std::list<std::string> m_data;
};

In log.cpp we need to add

std::list<std::string> Log::m_data;

乍一看确实具备单例模式的很多条件, 不过它也有一些问题. 第一, 静态成员变量初始化顺序不依赖构造函数, 得看编译器心情的, 没法保证初始化顺序 (极端情况: 有 a b 两个成员对象, b 需要把 a 作为初始化参数传入, 你的类就 必须 得要有构造函数, 并确保初始化顺序).

第二, 最严重的问题, 失去了面对对象的重要特性 — “多态”, 静态成员方法不可能是 virtual 的. Log 类的子类没法享受 “多态” 带来的便利.

饿汉模式

饿汉模式 是指单例实例在程序运行时被立即执行初始化:

class Log {
public:
  static Log* Instance() {
    return &m_pInstance;
  }

  virtual void Write(char const *logline);
  virtual bool SaveTo(char const *filename);

private:
  Log();              // ctor is hidden
  Log(Log const&);    // copy ctor is hidden

  static Log m_pInstance;
  static std::list<std::string> m_data;
};

// in log.cpp we have to add
Log Log::m_pInstance;

这种模式的问题也很明显, 类现在是多态的, 但静态成员变量初始化顺序还是没保证.

还引起另外一个问题 (我之前碰到过的真实事件, 以后便一直采用下面提到的 “懒汉模式”): 有两个单例模式的类 ASingletonBSingleton, 某天你想在 BSingleton 的构造函数中使用 ASingleton 实例, 这就出问题了. 因为 BSingleton m_pInstance 静态对象可能先 ASingleton 一步调用初始化构造函数, 结果 ASingleton::Instance() 返回的就是一个未初始化的内存区域, 程序还没跑就直接崩掉.

懒汉模式 (堆栈-粗糙版)

J. Nakamura 把它叫作 “Gamma Singleton”, 因为这是 Gamma 在他大名鼎鼎的 <<设计模式>> (<<Design Patterns>>) [Gamma] 一书采用的方法. 称它为 “懒汉模式” 是因为单例实例只在第一次被使用时进行初始化:

class Log {

public:
  static Log* Instance() {
    if (!m_pInstance)
      m_pInstance = new Log;
    return m_pInstance;
  }

  virtual void Write(char const *logline);
  virtual bool SaveTo(char const *filename);

private:
  Log();        // ctor is hidden
  Log(Log const&);    // copy ctor is hidden

  static Log* m_pInstance;
  static std::list<std::string> m_data;
};

// in log.cpp we have to add
Log* Log::m_pInstance = NULL;

Instance() 只在第一次被调用时为 m_pInstance 分配内存并初始化. 嗯, 看上去所有的问题都解决了, 初始化顺序有保证, 多态也没问题.

不过细心的你可能已经发现了一个问题, 程序退出时, 析构函数没被执行. 这在某些设计不可靠的系统上会导致资源泄漏, 比如文件句柄, socket 连接, 内存等等. 幸好 Linux / Windows 2000/XP 等常用系统都能在程序退出时自动释放占用的系统资源. 不过这仍然可能是个隐患, 至少 J. Nakamura 印象中, 有些系统是不会自动释放的.

对于这个问题, 比较土的解决方法是, 给每个 Singleton 类添加一个 destructor() 方法:

virtual bool destructor() {
    // ... release resource

    if (NULL!= m_pInstance) {
        delete m_pInstance;
        m_pInstance = NULL;
    }
}

然后在程序退出时确保调用了每个 Singleton 类的 destructor() 方法, 这么做虽然可靠, 但却很是繁琐. 幸运的是, Meyers 大师有个更简便的方法.

懒汉模式 (局部静态变量-最佳版)

它也被称为 Meyers Singleton [Meyers]:

class Log {
public:
  static Log& Instance() {
    static Log theLog;
    return theLog;
  }

  virtual void Write(char const *logline);
  virtual bool SaveTo(char const *filename);

private:
  Log();          // ctor is hidden
  Log(Log const&);      // copy ctor is hidden
  Log& operator=(Log const&);  // assign op is hidden

  static std::list<std::string> m_data;
};

Instance() 函数内定义局部静态变量的好处是, theLog 的构造函数只会在第一次调用 “Instance() 时被初始化, 达到了和 “堆栈版” 相同的动态初始化效果, 保证了成员变量和 Singleton 本身的初始化顺序.

它还有一个潜在的安全措施, Instance() 返回的是对局部静态变量的引用, 如果返回的是指针, Instance() 的调用者很可能会误认为他要检查指针的有效性, 并负责销毁. 构造函数和拷贝构造函数也私有化了, 这样类的使用者不能自行实例化.

另外, 多个不同的 Singleton 实例的析构顺序与构造顺序相反.

范例代码和注意事项 (最优实现)

把下面 C++ 代码片段中的 Singleton 替换成实际类名, 快速得到一个单例类:

class Singleton {
public:
    static Singleton& Instance() {
        static Singleton theSingleton;
        return theSingleton;
    }

    /* more (non-static) functions here */

private:
    Singleton();                            // ctor hidden
    Singleton(Singleton const&);            // copy ctor hidden
    Singleton& operator=(Singleton const&); // assign op. hidden
    ~Singleton();                           // dtor hidden
};

Note

  • 任意两个 Singleton 类的构造函数不能相互引用对方的实例, 否则会导致程序崩溃. 如:

    ASingleton& ASingleton::Instance() {
        const BSingleton& b = BSingleton::Instance();
        static ASingleton theSingleton;
        return theSingleton;
    }
    
    BSingleton& BSingleton::Instance() {
        const ASingleton & b = ASingleton::Instance();
        static BSingleton theSingleton;
        return theSingleton;
    }
    
  • 在多线程的应用场合下必须小心使用. 如果唯一实例尚未创建时, 有两个线程同时调用创建方法, 且它们均没有检测到唯一实例的存在, 便会同时各自创建一个实例, 这样就有两个实例被构造出来, 从而违反了单例模式中实例唯一的原则. 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁 (虽然这样会降低效率).

  • 多个 Singleton 实例相互引用的情况下, 需要谨慎处理析构函数. 如: 初始化顺序为 ASingleton » BSingleton » CSingleton 的三个 Singleton 类, 其中 ASingleton BSingleton 的析构函数调用了 CSingleton 实例的成员函数, 程序退出时, CSingleton 的析构函数 将首先被调用, 导致实例无效, 那么后续 ASingleton BSingleton 的析构都将失败, 导致程序异常退出.

扩展阅读

  • 反模式 : 在实践中明显出现但又低效或是有待优化的设计模式, 是用来解决问题的带有共同性的不良方法. 它们已经经过研究并分类, 以防止日后重蹈覆辙, 并能在研发尚未投产的系统时辨认出来. (其中的一些反模式还挺有意思的);
  • C++ In Theory: The Singleton Pattern, Part 2 : “C++ In Theory” 系列的第二部分, 主要内容是泛型编程中的单例模式, 我对泛型不太感冒, 感兴趣的朋友可以看看.

参考资料

[Gamma]Design Patterns: E.Gamma, R.Helm, R.Johnson and J.Vlissides.
[Meyers]More Effective C++: S.Meyers.
2009-06-4, Thursday 16:37 PM | 0 comments | 0 pingbacks | Tags: , ,

CMake — 优秀的 C/C++ 构建系统

C/C++ 构建的特殊性

相比其它语言,C/C++ 的编译过程比较复杂,依赖具体的编译器、操作系统、硬件环境,所以一直以来缺乏一个简单易用、跨平台、支持多种编译器的通用构建工具。

由于工作需要,我前不久在找一个合适的通用构建工具。我的个人观点是,一个好的通用构建工具应该符合下面五个标准:

  1. 简单易用: 构建脚本的语法规则应该尽量简明,能够快速上手,有一个平滑的学习曲线。太高的学习和维护成本会抵消通用构建工具带来的好处;
  2. 自动维护依赖关系: 以前是手工+GCC -MF 开关维护,这种方法很慢、很土,而且项目一大,很容易出问题.
  3. 支持自动配置 (自动匹配出当前的操作系、编译器、程序库位置等环境);
  4. 稳定、高效;
  5. 可扩展: 有方法能够通过扩展,支持更多的编译器或系统 (如嵌入式).

对目前主流、仍然活跃的通用构建工具做了番仔细的对比和试用后, CMake 是最符合以上要求。

传统 C/C++ 构建工具

GNU make、NMake、Autotools 等传统构建工具,它们大都历史悠久、使用广泛、稳定可靠,但难用、不够智能。

它们在使用场合也有很大的局限性(比如 NMake 只支持 VCGNU make、Autotools 只能跑在 Linux/Cygwin 下),所以不在考虑之列。

SCons

出于对 Python 的偏好, 我用了一段时间的 SCons. SCons 是一个优秀的编译工具,不过距离构建还有一些差距:项目小的话还好,规模一大,依赖分析速度急速下降,而且自动配置功能很弱 (跨平台构建能力不足)。

Waf

WafCMake 的了解都来自同一条消息: KDE4 将采用 CMake 来替换 autotools(KDE4 也曾考虑过 SCons)。Waf 尝试解决 SCons 所暴露出的一些问题。和 SCons 一样,Waf 使用 Python 语言作为构建脚本。

不过对于构建这样简单而重要的工作而言,它显得太灵活了。一个任务可能有多种途径可以解决 — 用 Python 语句或 Waf 自定义规则。构建脚本用 Python 这么强大的语言,多少感觉有些杀鸡用牛刀 … 而且你要努力说服一些 Ruby funs 去学 Python。

Jam/Boost.Jam

跨平台工具,Boost 用 Jam 来维护整个库的编译和测试。

没有深入使用过,没有特别的杀手锏,最初的目标是取代 make 工具。没有自动配置功能,能适应简单的构建任务。我个人感觉它的语法同样很不直观。

CMake

看完 KDE 团队关于为何选择 CMake 的文章、评论后,我在一个项目中开始尝试使用 CMake。令人吃惊的是:和 KDE 组织所描述的一样,我只花了一天便掌握了 CMake 的基本用法。两天后我都快成了 CMake 半桶水专家!这比学习 GNU Make 工具的用法还快很多.

我之前想当然的认为 Waf 学习成本很低(我熟悉 Python 语法),但事实并非如此。Waf 新定义的一套规则仍然会占用你一段时间来适应 — 而且不比学习 CMake 的简单语法来的更短,里面夹杂着标准库会让不熟悉 Python 的成员很头痛。

CMake 的语法和规则足够简单,不需要花太多的时间,能很好的应付构建中碰到的各种问题。它不是最强大的,但它是最适合,也是最简单的。更复杂的功能可以用其它脚本语言(如 Python,Perl 等)配合完成。

这种方式明确了每个工具的职责 — CMake 只解决构建问题,而且重点是 C/C++ 的构建。相对于 Waf 这种能在构建脚本里面实现许多令人惊叹,只用 CMake 根本无法实现的超级工具而言,CMake 更直接、清晰,更擅长解决构建问题,并和其它工具协调工作。

有时候我们不需要一个超级工具,需要的只是一个最能解决当前问题的工具;过度的灵活常常导致过度的复杂 (设计和使用上)。

CMake 的不足

CMake 是用 C/C++ 写的。这当然是它的好处:处理速度很快,对于日常工作,效率是很重要的。不过要是我扩展 CMake 的内置功能,比如编写一个新的模块用于生成 Eclipse CDT(2.6 提供了)的工程,我想我会更乐意用 Python 或其它脚本语言来实现。

不过幸好 CMake 的宏机制能做一些比较简单的扩展,比如支持新的平台和编译器(PCLint、vxWorks GCC 等)。

如果能修补一些小问题, Bakefile 也是一个比较不错的工具。它的原理和 CMake 类似,只是它使用 XML 格式而不是 CMake 专用语法(不过个人感觉 CMake 的宏语法比机器友好的 XML 标记更清晰)。它是用 Python 写的,这也意味着它更容易扩展。不过它缺乏自动配置功能。

后记

开发和日构建需要的构建脚本不同:日构建的时候我们要的是 Makefile,但开发的时候我们需要工程文件。

和 SCons/Waf 等工具不同,CMake 可以用来生成 Makefile,还能生成 Visual Studio 等 IDE 的工程文件。

在项目上用 CMake 替换原来的 Makefile + 工程文件的方式后,整个构建和维护过程都比原先顺畅了许多。

配合使用 .bat 文件,我们用一份结构清晰的 CMake 构建脚本同时维护着 VC 6.0/2005 工程文件、NMake file 和 vxWorks GCC 交叉编译 Makefile,源文件的增删和编译参数的修改非常方便,节省了开发人员在软件构建上的时间成本。

2008-05-7, Wednesday 17:43 PM | 1 comment | 0 pingbacks | Tags: ,

关于 Yang Yubo

身处 IT 行业, 惯用 C/C++, 对 Python 情有独钟, 平时比较关注 QT / Django / Werkzeug 等. 了解更多 »