首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >现代C++单例模板

现代C++单例模板
EN

Code Review用户
提问于 2017-08-25 07:14:03
回答 3查看 56.7K关注 0票数 18

我最近读到了关于C++17 static inline成员声明的文章,并认为这将使模板更加简洁,因为静态成员现在可以在模板化的类中初始化。

因此,我想创建一个整洁的Singleton模板(因为它是需要静态成员的完美示例)。

现在来问我的问题:是否有一些我可能错过的东西,即是否有可能创建派生的Singleton副本?在一般的单例中使用CRTP是个好主意吗?关于move constructor,我也需要处理它吗?

下面是模板:

代码语言:javascript
复制
template < typename T >
class Singleton {
  public:
    static T& GetInstance() {
      static MemGuard g; // clean up on program end
      if (!m_instance) {
        m_instance = new T(); 
      }
      return *m_instance;
    }

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

  protected:
    Singleton() { };
    virtual ~Singleton() { }

  private:
    inline static T * m_instance = nullptr;

    class MemGuard {
      public: 
        ~MemGuard() {
          delete m_instance;
          m_instance = nullptr;
        }
    };
};

这里有一种可能的派生类型:

代码语言:javascript
复制
class Test final : public Singleton<Test> {
  friend class Singleton<Test>;
  public:
    void TestIt() { };
  private:
    Test() {}  
    ~Test() { /* Test intern clean up */ }
};
EN

回答 3

Code Review用户

回答已采纳

发布于 2017-08-25 08:45:58

好的,首先是强制性的单身是不好的做法,所以您可能不应该让编写不好的代码变得容易。

忽略了类可能根本不存在的事实,我们可以查看代码。

代码语言:javascript
复制
static T& GetInstance() {
  static MemGuard g; // clean up on program end
  if (!m_instance) {
    m_instance = new T(); 
  }
  return *m_instance;
}

如果多个线程在创建该实例之前同时访问该实例,则存在数据争用,并且m_instance可能会被多次构造或其他类型的未定义行为。您需要在if块周围添加互斥锁,或者使用首选的std::call_once

由于它应该是单个实例,所以不能创建更多的实例,因为单个实例的含义是只有一个实例,但似乎完全有可能通过将多个实例创建为局部变量来构造Test的多个实例。这是模板中的一个设计缺陷。

创建单例的一个更好的方法是依赖C++11 N2660(魔术静力学)。只需这样做:

代码语言:javascript
复制
class Test{
private:
    Test(); // Disallow instantiation outside of the class.
public:
    Test(const Test&) = delete;
    Test& operator=(const Test &) = delete;
    Test(Test &&) = delete;
    Test & operator=(Test &&) = delete;

    static auto& instance(){
        static Test test;
        return test;
    }
}; 

这比编写代码容易得多,它是线程安全的,并且解决了允许实例化Test的问题。魔法静力学的特性保证了当函数体第一次被任何线程输入时,test都会被初始化,即使在多个线程存在的情况下也是如此,否则可能会导致数据竞争。当main()函数返回(在静态销毁阶段)时,实例将被解构,这使得整个MemGuard变得不必要。

票数 18
EN

Code Review用户

发布于 2017-08-25 11:30:32

单例会使测试代码变得困难,在我的工作中,我会因为鼓励开发不可测试的特性而拒绝这一点。尽管如此,我还是会继续复习。

不需要帮助类

MemGuard似乎是穷人对std::unique_ptr的重新实现。对于您来说,将m_instance声明为std::unique_ptr<T>要简单得多,然后从您的访问器返回*m_instance

当两个或多个线程试图创建实例时(当两个或多个线程都在“另一个线程设置”之前看到一个空指针时),就存在一个争用条件。您可以使用互斥锁来解决这个问题,但是使用本地静态变量它是线程安全的更简单:

代码语言:javascript
复制
#include <memory>
template<typename T>
T& Singleton<T>::instance()
{
    static const std::unique_ptr<T> instance{new T{}};
    return *instance;
}

我们不需要析构函数

不需要空的虚拟析构函数,因为构造的对象将始终作为其声明的类型被删除。

修改后的实现

通过我的更改,代码简化为

代码语言:javascript
复制
template<typename T>
class Singleton {
public:
    static T& instance();

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

protected:
    struct token {};
    Singleton() {}
};

#include <memory>
template<typename T>
T& Singleton<T>::instance()
{
    static const std::unique_ptr<T> instance{new T{token{}}};
    return *instance;
}

我使用构造函数令牌来允许基类调用子类的构造函数,而不需要是friend

示例

一个示例T看起来如下:

代码语言:javascript
复制
#include <iostream>
class Test final : public Singleton<Test>
{
public:
    Test(token) { std::cout << "constructed" << std::endl; }
    ~Test() {  std::cout << "destructed" << std::endl; }

    void use() const { std::cout << "in use" << std::endl; };
};

尽管构造函数是公共的,但是没有Singleton<T>::token对象就不能调用它,这意味着对它的访问现在是受控的。

测试:

代码语言:javascript
复制
int main()
{
    // Test cannot_create; /* ERROR */

    std::cout << "Entering main()" << std::endl;
    {
        auto const& t = Test::instance();
        t.use();
    }
    {
        auto const& t = Test::instance();
        t.use();
    }
    std::cout << "Leaving main()" << std::endl;
}
代码语言:javascript
复制
Entering main()
constructed
in use
in use
Leaving main()
destructed

Afterthought:

不需要智能指针;普通内存管理在这里工作:

代码语言:javascript
复制
template<typename T>
T& Singleton<T>::instance()
{
    static T instance{token{}};
    return instance;
}
票数 14
EN

Code Review用户

发布于 2019-06-22 08:03:38

在使用静态和共享库时,必须小心,因为您没有几个instance()函数的实现。这将导致在实际存在多个实例的情况下很难调试错误。为了避免这种情况,请使用编译单元(.cpp)中的实例函数,而不是头文件中的模板。

票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/173929

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档