C++ 单例模式的模板实现

C++ 单例模式的模板实现

单例模式是一种创建型的设计模式(creational design patterns),使用单例模式进行设计的类在程序中只拥有一个实例(single instance),这个类称为单例类,它会提供一个全局的访问入口(global access point),关于单例模式的讨论可以参考Singleton revisited;基于这两个特点,单例模式可以有以下几种实现:

注意这种实例是通过返回引用使用的。所以需这样使用:Favorite& fav = Favorite::GetInstance(); 否则 Favorite fav = Favorite::GetInstance(); 会执行instance到fav这个新变量的赋值操作!!!不是单个实例了。

skia上单例设计:

使用:

sk_sp<SkFontMgr> SkFontMgr::RefDefault() {
    static SkOnce once;
    static sk_sp<SkFontMgr> singleton;

    once([]{//下面设个lamda函数
        sk_sp<SkFontMgr> fm = gSkFontMgr_DefaultFactory ? gSkFontMgr_DefaultFactory()
                                                        : SkFontMgr::Factory();
        singleton = fm ? std::move(fm) : sk_make_sp<SkEmptyFontMgr>();
    });
    return singleton;
}

 

将once做成一个仿函数,它接收一个lamda函数,做一次调用。

once实现源码:

在进入仿函数调用时,通过原子量atom去判断状态,done时直接返回,说明已经初始化好了。取这个done在死循环中,因为取的时候有可能另外一个线程正在构造实例,这时处于climed状态。

如果没有start,那就进入clime状态,开始构造单例。

 /*
 * Copyright 2013 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */
 
#ifndef SkOnce_DEFINED
#define SkOnce_DEFINED
 
#include "include/private/SkThreadAnnotations.h"
#include <atomic>
#include <utility>
 
// SkOnce provides call-once guarantees for Skia, much like std::once_flag/std::call_once().
//
// There should be no particularly error-prone gotcha use cases when using SkOnce.
// It works correctly as a class member, a local, a global, a function-scoped static, whatever.
 
class SkOnce {
public:
    constexpr SkOnce() = default;
 
    template <typename Fn, typename... Args>
    void operator()(Fn&& fn, Args&&... args) {
        auto state = fState.load(std::memory_order_acquire);
 
        if (state == Done) {
            return;
        }
 
        // If it looks like no one has started calling fn(), try to claim that job.
        if (state == NotStarted && fState.compare_exchange_strong(state, Claimed,
                                                                  std::memory_order_relaxed,
                                                                  std::memory_order_relaxed)) {
            // Great!  We'll run fn() then notify the other threads by releasing Done into fState.
            fn(std::forward<Args>(args)...);
            return fState.store(Done, std::memory_order_release);
        }
 
        // Some other thread is calling fn().
        // We'll just spin here acquiring until it releases Done into fState.
        SK_POTENTIALLY_BLOCKING_REGION_BEGIN;
        while (fState.load(std::memory_order_acquire) != Done) { /*spin*/ }
        SK_POTENTIALLY_BLOCKING_REGION_END;
    }
 
private:
    enum State : uint8_t { NotStarted, Claimed, Done};
    std::atomic<uint8_t> fState{NotStarted};
};
 
#endif  // SkOnce_DEFINED

 

Meyer's Singleton

Scott Meyers 在 Effective C++ 的 Item 4: Make sure that objects are initialized before they're used 里面提出了一种利用 C++ 的 static 关键字来实现的单例模式,这种实现非常简洁高效,它的特点是:

  1. 仅当程序第一次执行到 GetInstance 函数时,执行 instance 对象的初始化;
  2. 在 C++ 11 之后,被 static 修饰的变量可以保证是线程安全的;
template<typename T>
class Singleton
{
public:
    static T& GetInstance()
    {
        static T instance;
        return instance;
    }

    Singleton(T&&) = delete;
    Singleton(const T&) = delete;
    void operator= (const T&) = delete;

protected:
    Singleton() = default;
    virtual ~Singleton() = default;
};

通过禁用单例类的 copy constructor,move constructor 和 operator= 可以防止类的唯一实例被拷贝或移动;不暴露单例类的 constructor 和 destructor 可以保证单例类不会通过其他途径被实例化,同时将两者定义为 protected 可以让其被子类继承并使用。

Lazy Singleton

Lazy Singleton 是一种比较传统的实现方法,通过其名字可以看出来它也具有 lazy-evaluation 的特点,但在实现的时候需要考虑线程安全的问题:

template<typename T, bool is_thread_safe = true>
class LazySingleton
{
private:
    static unique_ptr<T> t_;
    static mutex mtx_;

public:
    static T& GetInstance()
    {
        if (is_thread_safe == false)
        {
            if (t_ == nullptr)
                t_ = unique_ptr<T>(new T);
            return *t_;
        }

        if (t_ == nullptr)
        {
            unique_lock<mutex> unique_locker(mtx_);
            if (t_ == nullptr)
                t_ = unique_ptr<T>(new T);
            return *t_;
        }

    }

    LazySingleton(T&&) = delete;
    LazySingleton(const T&) = delete;
    void operator= (const T&) = delete;

protected:
    LazySingleton() = default;
    virtual ~LazySingleton() = default;
};

template<typename T, bool is_thread_safe>
unique_ptr<T> LazySingleton<T, is_thread_safe>::t_;

template<typename T, bool is_thread_safe>
mutex LazySingleton<T, is_thread_safe>::mtx_;

我们通过模板参数 is_thread_safe 来控制这个类是否是线程安全的,因为在某些场景下我们会希望每个线程拥有一个实例:

  1. 当 is_thread_safe == false,即非线程安全时,我们在 GetInstance 函数中直接判断,初始化并返回单例对象;这里使用了 unique_ptr 防止线程销毁时发生内存泄漏,也可以在析构函数中销毁指针;
  2. 当 is_thread_safe == true 时,我们通过 double-checked locking 来进行检查并加锁,防止单例类在每个线程上都被实例化。

Eager Singleton

和 Lazy Singleton 相反,Eager Singleton 利用 static member variable 的特性,在程序进入 main 函数之前进行初始化,这样就绕开了线程安全的问题:

template<typename T>
class EagerSingleton
{
private:
    static T* t_;

public:
    static T& GetInstance()
    {
        return *t_;
    }

    EagerSingleton(T&&) = delete;
    EagerSingleton(const T&) = delete;
    void operator= (const T&) = delete;

protected:
    EagerSingleton() = default;
    virtual ~EagerSingleton() = default;
};

template<typename T>
T* EagerSingleton<T>::t_ = new (std::nothrow) T;

但是它也有两个问题:

  1. 即使单例对象不被使用,单例类对象也会进行初始化;
  2. static initialization order fiasco,即 t_ 对象和 GetInstance 函数的初始化先后顺序是不固定的;

Testing

将上面实现的四种 Singleton 分别继承下来作为 functor 传入线程对象进行测试:

class Foo : public Singleton<Foo>
{
public:
    void operator() ()
    {
        cout << &GetInstance() << endl;
    }
};

class LazyFoo : public LazySingleton<LazyFoo, false>
{
public:
    void operator() ()
    {
        cout << &GetInstance() << endl;
    }
};

class ThreadSafeLazyFoo : public LazySingleton<ThreadSafeLazyFoo>
{
public:
    void operator() ()
    {
        cout << &GetInstance() << endl;
    }
};

class EagerFoo : public EagerSingleton<EagerFoo>
{
public:
    void operator() ()
    {
        cout << &GetInstance() << endl;
    }
};

void SingletonTest()
{
    thread t1((Foo()));
    thread t2((Foo()));
    t1.join();
    t2.join();
    this_thread::sleep_for(chrono::milliseconds(100));

    t1 = thread((LazyFoo()));
    t2 = thread((LazyFoo()));
    t1.join();
    t2.join();
    this_thread::sleep_for(chrono::milliseconds(100));

    t1 = thread((ThreadSafeLazyFoo()));
    t2 = thread((ThreadSafeLazyFoo()));
    t1.join();
    t2.join();
    this_thread::sleep_for(chrono::milliseconds(100));

    t1 = thread((EagerFoo()));
    t2 = thread((EagerFoo()));
    t1.join();
    t2.join();
}

输出结果为:

0x60d110
0x60d110
0x7f92380008c0
0x7f92300008c0
0x7f92300008e0
0x7f92300008e0
0x1132010
0x1132010

可以看到只有第二组非线程安全的 LazySingleton 在两个线程中输出的实例地址是不同的,其它的 Singleton 均是线程安全的。

 

注意thread调用,传递进去的是个仿函数。即 Foo()是调用构造函数。Foo()()对对象执行()调用才是调用的()函数。

参考:

C++中如何区分构造函数与重载operator()得到的仿函数

首先是定义形式:

  1. 构造函数无返回值,而operator是可以有返回值的;

  2. 定义时,构造函数需要名,而重载operator()则不用;

其次是调用形式:

构造函数是声明对象,而仿函数则需要声明好的对象进行调用。

 

functor是仿函数,function是函数还是std::function? 

如果是函数的话,个人感觉不会替代。因为毕竟C++是C的超集,而C中很多用到函数指针的地方,functor可能并不那么好用。

如果是std::function的话,那感觉也不会替代,因为std::function配合std::bind可以处理函数及仿函数。bind()接受一个函数(或者函数对象,或者任何你可以通过”(…)”符号调用的事物),生成一个其有某一个或多个函数参数被“绑定”或重新组织的函数对象。而functor好像不能以std::function作为参数传入。

话说仿函数即函数对象书写比较费事,所以C++ 11增加了lambda表达式,即匿名函数。函数对象即仿函数多用于C++ STL中,而在微软新加入的并行库PPL中,task等并不接受仿函数,多用lambda表达式。更容易。

 

posted @ 2021-03-11 14:57  Bigben  阅读(895)  评论(0)    收藏  举报