我最近读到了关于C++17 static inline成员声明的文章,并认为这将使模板更加简洁,因为静态成员现在可以在模板化的类中初始化。
因此,我想创建一个整洁的Singleton模板(因为它是需要静态成员的完美示例)。
现在来问我的问题:是否有一些我可能错过的东西,即是否有可能创建派生的Singleton副本?在一般的单例中使用CRTP是个好主意吗?关于move constructor,我也需要处理它吗?
下面是模板:
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;
}
};
};这里有一种可能的派生类型:
class Test final : public Singleton<Test> {
friend class Singleton<Test>;
public:
void TestIt() { };
private:
Test() {}
~Test() { /* Test intern clean up */ }
};发布于 2017-08-25 08:45:58
好的,首先是强制性的单身是不好的做法,所以您可能不应该让编写不好的代码变得容易。
忽略了类可能根本不存在的事实,我们可以查看代码。
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(魔术静力学)。只需这样做:
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变得不必要。
发布于 2017-08-25 11:30:32
单例会使测试代码变得困难,在我的工作中,我会因为鼓励开发不可测试的特性而拒绝这一点。尽管如此,我还是会继续复习。
MemGuard似乎是穷人对std::unique_ptr的重新实现。对于您来说,将m_instance声明为std::unique_ptr<T>要简单得多,然后从您的访问器返回*m_instance。
当两个或多个线程试图创建实例时(当两个或多个线程都在“另一个线程设置”之前看到一个空指针时),就存在一个争用条件。您可以使用互斥锁来解决这个问题,但是使用本地静态变量它是线程安全的更简单:
#include <memory>
template<typename T>
T& Singleton<T>::instance()
{
static const std::unique_ptr<T> instance{new T{}};
return *instance;
}不需要空的虚拟析构函数,因为构造的对象将始终作为其声明的类型被删除。
通过我的更改,代码简化为
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看起来如下:
#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对象就不能调用它,这意味着对它的访问现在是受控的。
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;
}Entering main()
constructed
in use
in use
Leaving main()
destructed不需要智能指针;普通内存管理在这里工作:
template<typename T>
T& Singleton<T>::instance()
{
static T instance{token{}};
return instance;
}发布于 2019-06-22 08:03:38
在使用静态和共享库时,必须小心,因为您没有几个instance()函数的实现。这将导致在实际存在多个实例的情况下很难调试错误。为了避免这种情况,请使用编译单元(.cpp)中的实例函数,而不是头文件中的模板。
https://codereview.stackexchange.com/questions/173929
复制相似问题