Muduo库中单例模式详解

最近在阅读muduo库的单例源码时发现其中Singleton模板的实现,简直堪称C++的编程艺术品。这里就记录一下从这其中所学到的一些思想。

(其余详细的单例模式介绍可以参考这篇文章[设计模式]C++单例模式的几种写法以及通用模板-CSDN博客

首先贴出其源码:

网址在这muduo/muduo/base/Singleton.h at master · chenshuo/muduo

cpp 复制代码
// Use of this source code is governed by a BSD-style license
// that can be found in the License file.
//
// Author: Shuo Chen (chenshuo at chenshuo dot com)

#ifndef MUDUO_BASE_SINGLETON_H
#define MUDUO_BASE_SINGLETON_H

#include "muduo/base/noncopyable.h"

#include <assert.h>
#include <pthread.h>
#include <stdlib.h> // atexit

namespace muduo
{

namespace detail
{
// This doesn't detect inherited member functions!
// http://stackoverflow.com/questions/1966362/sfinae-to-check-for-inherited-member-functions
template<typename T>
struct has_no_destroy
{
  template <typename C> static char test(decltype(&C::no_destroy));
  template <typename C> static int32_t test(...);
  const static bool value = sizeof(test<T>(0)) == 1;
};
}  // namespace detail

template<typename T>
class Singleton : noncopyable
{
 public:
  Singleton() = delete;
  ~Singleton() = delete;

  static T& instance()
  {
    pthread_once(&ponce_, &Singleton::init);
    assert(value_ != NULL);
    return *value_;
  }

 private:
  static void init()
  {
    value_ = new T();
    if (!detail::has_no_destroy<T>::value)
    {
      ::atexit(destroy);
    }
  }

  static void destroy()
  {
    typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];
    T_must_be_complete_type dummy; (void) dummy;

    delete value_;
    value_ = NULL;
  }

 private:
  static pthread_once_t ponce_;
  static T*             value_;
};

template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;

template<typename T>
T* Singleton<T>::value_ = NULL;

}  // namespace muduo

#endif  // MUDUO_BASE_SINGLETON_H

Muduo 库中的单例实现非常经典和巧妙,它不仅仅是提供一个全局唯一的实例,更通过多种 C++ 高级技巧解决了一系列传统单例模式中常见的问题。其核心思想可以概括为以下三点

  1. 利用 pthread_once 实现线程安全的懒汉式初始化

    • 问题:传统的懒汉式单例在多线程环境下存在竞态条件,需要加锁保护,但加锁会带来性能开销。而饿汉式(程序启动时即创建)虽然线程安全,但无法解决跨编译单元的静态对象初始化顺序问题。

    • Muduo 的方案 :Muduo 采用了 POSIX 提供的 pthread_once 机制。该函数能保证在整个进程的生命周期内,其指定的初始化函数 (init) 绝对只会被执行一次,即使有多个线程在同一时刻尝试调用它 。它自带了高效的、无竞争的线程安全保障,完美地解决了多线程环境下的初始化问题,且避免了初始化顺序依赖的麻烦。

  2. 利用 SFINAE 技巧实现灵活的生命周期管理

    • 问题:单例对象的销毁时机是一个难题。通常它会在程序结束时自动销毁,但有时我们希望在程序运行中途手动控制其销毁。

    • Muduo 的方案 :它使用了一种名为 SFINAE (Substitution Failure Is Not An Error, 替换失败并非错误) 的高级 C++ 模板元编程技巧 。

      • 它在模板内部通过 sizeof 和模板重载,在编译期检查用户提供的类(如 IMServer)是否定义了一个名为 no_destroy_ 的成员。

      • 如果用户没有定义 no_destroy_,单例模板就会自动注册一个 destroy 函数,在程序退出时通过 atexit 调用 delete 来销毁实例。

      • 如果用户定义了 no_destroy_,SFINAE 规则会使自动销毁的代码路径在编译时被"忽略",从而将销毁的责任交还给用户。

      • 这种方式让使用者可以自由选择由单例模板自动管理生命周期,还是由自己手动管理,极大地提高了灵活性 。

  3. 利用 sizeof 在编译期进行静态断言

    • 问题:如果用户尝试将一个"不完整类型"(例如只有一个前向声明的类)实例化为单例,并在程序退出时尝试 delete 它,会导致未定义行为,这通常是运行时才能发现的严重错误。

    • Muduo 的方案:在 destroy 函数中,它使用 typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1]; 这样的代码进行检查。

    • 如果T 是一个不完整的类型,sizeof(T) 的结果在某些编译器上是0。此时,代码试图定义一个大小为-1的数组,这在语法上是错误的,直接导致编译失败

    • 这巧妙地将一个潜在的、危险的运行时错误,提前暴露为了一个编译期错误,大大增强了代码的健壮性和安全性 。

cpp 复制代码
#ifndef MUDUO_BASE_SINGLETON_H
#define MUDUO_BASE_SINGLETON_H

#include <muduo/base/noncopyable.h>
#include <assert.h>
#include <pthread.h>
#include <stdlib.h> // atexit

namespace muduo
{

namespace detail
{
// 这是一个模板元编程技巧,用于在编译期检查类型 T 是否含有 no_destroy_ 成员。
template<typename T>
struct has_no_destroy
{
  // 模板重载决议:尝试匹配这个版本。
  // &C::no_destroy_ 会检查 C 类型中是否存在名为 no_destroy_ 的成员。
  // 如果存在,这个模板函数就能被成功实例化。
  template <typename C> static char test(decltype(&C::no_destroy_));
  
  // 模板重载决议:如果上面的版本匹配失败(即替换失败),编译器不会报错,而是会尝试这个版本。 
  // "..." 是可变参数模板,可以匹配任何类型的任意数量参数,所以它总能匹配成功。
  template <typename C> static int32_t test(...);
  
  // const bool value = sizeof(test<T>(0)) == 1;
  // 核心思想:调用test<T>(0)。
  // 如果 T 类型有 no_destroy_ 成员,第一个 test 函数匹配成功,返回类型是 char,sizeof(char) == 1。
  // 如果 T 类型没有 no_destroy_ 成员,第一个 test 函数替换失败,匹配第二个,返回类型是 int32_t,sizeof(int32_t) == 4。
  // 因此,通过判断返回值的大小,就可以在编译期确定 T 是否有 no_destroy_ 成员。
  const static bool value = sizeof(test<T>(0)) == 1;
};
}

template<typename T>
class Singleton : noncopyable // 继承 noncopyable 以禁止拷贝构造和赋值操作
{
 public:
  Singleton() = delete;   // 禁止构造
  ~Singleton() = delete;  // 禁止析构

  static T& instance()
  {
    // pthread_once 保证在多线程环境下,init() 函数只会被执行一次。
    // 它解决了线程安全的懒汉式初始化问题,且没有性能锁的开销。
    pthread_once(&ponce_, &Singleton::init);
    assert(value_ != NULL);
    return *value_;
  }

 private:
  static void init()
  {
    value_ = new T(); // 创建单例对象实例
    // 使用 SFINAE 技巧判断用户类 T 是否自定义了 no_destroy_ 成员。
    if (!detail::has_no_destroy<T>::value)
    {
      // 如果没有定义 no_destroy_,则注册 atexit(destroy),让程序在退出时自动销毁单例。
      ::atexit(destroy);
    }
  }

  static void destroy()
  {
    // 这是一个编译期断言技巧,用于确保 T 是一个完整类型。
    // 如果 T 是不完整类型(如只有前向声明),sizeof(T) 会是 0,导致试图定义一个大小为 -1 的数组,从而引发编译错误。 
    // 这将潜在的运行时风险(删除不完整类型)转移到了编译期。 
    typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];
    (void) sizeof(T_must_be_complete_type); // 防止编译器警告未使用该类型定义
    delete value_;
    value_ = NULL;
  }

 private:
  // pthread_once 要求一个静态的 pthread_once_t 控制变量
  static pthread_once_t ponce_;
  // 全局唯一的实例指针
  static T* value_;
};

// 静态成员变量的类外初始化
template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT; // 初始化 pthread_once 控制变量

template<typename T>
T* Singleton<T>::value_ = NULL;

}
#endif  // MUDUO_BASE_SINGLETON_H
相关推荐
GISer_Jing3 小时前
JavaScript 中Object、Array 和 String的常用方法
开发语言·javascript·ecmascript
耳总是一颗苹果4 小时前
C语言---动态内存管理
c语言·开发语言
手眼通天王水水5 小时前
【Linux】3. Shell语言
linux·运维·服务器·开发语言
仟濹5 小时前
【数据结构】「队列」(顺序队列、链式队列、双端队列)
c语言·数据结构·c++
小蜗牛狂飙记5 小时前
在github上传python项目,然后在另外一台电脑下载下来后如何保障成功运行
开发语言·python·github
小苏兮5 小时前
【C语言】字符串与字符函数详解(上)
c语言·开发语言·算法
chilavert3185 小时前
技术演进中的开发沉思-40 MFC系列:多线程协作
c++·windows·mfc
程序员JerrySUN6 小时前
Valgrind Memcheck 全解析教程:6个程序说明基础内存错误
android·java·linux·运维·开发语言·学习
apocelipes6 小时前
使用uint64_t批量比较短字符串
c语言·数据结构·c++·算法·性能优化·golang
菜一头包6 小时前
C++ STL中迭代器学习笔记
c++·笔记·学习