【问题标题】:Singleton instance as static field vs. static variable in getInstance() method单例实例作为静态字段与 getInstance() 方法中的静态变量
【发布时间】:2015-12-21 13:03:38
【问题描述】:

this thread 中,关于单例实例如下所述:

静态变量对于 GetInstance() 函数可以是静态的,也可以在 Singleton 类中是静态的。那里有有趣的权衡。

这些权衡是什么?我知道,如果声明为static 函数变量,则在第一次调用该函数之前不会构造单例。我也读过一些关于线程安全的文章,但我不知道这究竟意味着什么,或者这两种方法在这方面有何不同。

两者之间还有其他主要区别吗?哪种方法更好?

在我的具体示例中,我将工厂类设置为单例,并将实例存储为类中的 static const 字段。我没有getInstance() 方法,而是希望用户直接访问实例,如下所示:ItemFactory::factory。默认构造函数是私有的,实例是静态分配的。

附录:重载operator() 以调用单例的createItem() 方法是多么好的一个想法,这样Items 可以像这样创建:ItemFactory::factory("id")

【问题讨论】:

  • 请参阅this 了解静态函数变量的线程安全性。我会说静态函数变量是要走的路,因为这样可以确保正确的初始化顺序。

标签: c++ design-patterns singleton c++14 static-members


【解决方案1】:

这些权衡是什么?

这是最重要的考虑因素:

static 数据成员在程序开始时的静态初始化期间被初始化。如果有任何static 对象依赖于单例,那么就会有一个static initialization order fiasco

函数本地static对象在函数第一次被调用时被初始化。由于依赖于单例的人都会调用该函数,因此单例将被适当地初始化并且不会受到惨败的影响。破坏仍然存在一个非常微妙的问题。如果静态对象的析构函数依赖于单例,但该对象的构造函数不依赖,那么您最终会得到未定义的行为。

另外,在函数第一次被调用时被初始化,意味着函数可以在静态初始化完成并且main被调用之后被调用。因此,该程序可能产生了多个线程。 static 本地的初始化可能存在竞争条件,从而导致构造多个实例。幸运的是,从 C++11 开始,该标准保证初始化是线程安全的,并且这种权衡不再存在于符合标准的编译器中。

static 数据成员不存在线程安全问题。

哪种方法更好?

这取决于您的要求以及您支持的标准版本。

【讨论】:

  • 我遇到了一个使用全局变量作为单例的库的问题,但由于较低级别的 O/S 重构,它的构造函数被初始化为使用有关系统图形功能的错误信息。他们在以后的版本中将其转换为“惰性”构造函数,即在第一次客户端调用之后,它就像一个魅力,因为这个单例所依赖的一切都真正初始化了。
【解决方案2】:

我投票支持静态函数变量。较新的 C++ 标准要求自动线程安全来初始化此类变量。它已经在 GNU C++ 中实现了大约十年。 Visual Studio 2015 也支持这一点。如果你创建一个静态指针变量来保存对你的单例对象的引用,你将不得不手动处理线程问题。

另一方面,如果你像下面的 sn-p 所示创建一个静态成员指针字段,你将能够从其他静态方法中更改它,也许在处理请求时用其他实例重新初始化这个字段更改程序配置。但是,下面的 sn-p 包含一个错误,只是为了提醒您多线程是多么困难。

class ItemFactory {
static std::atomic_flag initialized = ATOMIC_FLAG_INIT;
static std::unique_ptr<ItemFactory> theFactoryInstance;
public:
static ItemFactory& getInstance() {
  if (!initialized.test_and_set(std::memory_order_acquire)) {
    theFactoryInstance = std::make_unique<ItemFactory>();
  }
  return *theFactoryInstance;
}
};

我不建议您将单例实现为在进入main() 函数之前初始化的全局非指针变量。线程安全问题将随着隐式缓存一致性开销而消失,但您无法以任何精确或可移植的方式控制全局变量的初始化顺序。

无论如何,这种选择不会强制产生任何永久性的设计含义。由于此实例将驻留在您班级的private 部分,因此您可以随时更改它。

我不认为为工厂重载 operator() 是一个好主意。 operator() 具有“执行”语义,而在工厂中它将代表“创建”。

【讨论】:

  • 虽然我同意,但也许详细说明为什么静态函数变量是要走的路,因为 OP 要求权衡。
  • static 类字段有很多不同的应用程序,除了托管单例实例。类范围的属性,例如可动态重新配置的虚拟函数表替代品。
  • 这并不能真正回答问题这些权衡是什么?两者之间还有其他主要区别吗?哪种方法更好?
  • @NathanOliver:更新了答案,添加了静态变量的大小写。
【解决方案3】:

c++ 中单例的最佳方法是什么?

隐藏它是单例的事实并赋予它值语义。

怎么做?

所有的单例都应该是一个实现细节。这样,如果您需要更改实现单例的方式(或者实际上,如果您决定它不应该是真正的单例),您的类的使用者不需要重构他们的程序。

为什么?

因为现在您的程序不必担心引用、指针、生命周期等等。它只是使用对象的一个​​实例,就好像它是一个值一样。知道单例将处理它所具有的任何生命周期/资源要求,这是安全的。

在不使用时释放资源的单例呢?

没问题。

这是隐藏在具有值语义的对象的外观背后的两种方法的示例。

想象一下这个用例:

auto j1 = jobbie();
auto j2 = jobbie();
auto j3 = jobbie();

j1.log("doh");
j2.log("ray");
j3.log("me");

{
    shared_file f;
    f.log("hello");
}

{
    shared_file().log("goodbye");
}

shared_file().log("here's another");

shared_file f2;
{
    shared_file().log("no need to reopen");
    shared_file().log("or here");
    shared_file().log("or even here");
}
f2.log("all done");

jobbie 对象只是单例的外观,但 shared_file 对象希望在不使用时自行刷新/关闭。

所以输出应该是这样的:

doh
ray
me
opening file
logging to file: hello
closing file
opening file
logging to file: goodbye
closing file
opening file
logging to file: here's another
closing file
opening file
logging to file: no need to reopen
logging to file: or here
logging to file: or even here
logging to file: all done
closing file

我们可以使用成语来实现这一点,我称之为“value-semantics-is-a-facade-for-singleton”:

#include <iostream>
#include <vector>

// interface
struct jobbie
{
    void log(const std::string& s);

private:
    // if we decide to make jobbie less singleton-like in future
    // then as far as the interface is concerned the only change is here
    // and since these items are private, it won't matter to consumers of the class
    struct impl;
    static impl& get();
};

// implementation

struct jobbie::impl
{
    void log(const std::string& s) {
        std::cout << s << std::endl;
    }
};

auto jobbie::get() -> impl& {
    //
    // NOTE
    // now you can change the singleton storage strategy simply by changing this code
    // alternative 1:
    static impl _;
    return _;

    // for example, we could use a weak_ptr which we lock and store the shared_ptr in the outer
    // jobbie class. This would give us a shared singleton which releases resources when not in use

}

// implement non-singleton interface

void jobbie::log(const std::string& s)
{
    get().log(s);
}

struct shared_file
{
    shared_file();

    void log(const std::string& s);

private:
    struct impl;
    static std::shared_ptr<impl> get();
    std::shared_ptr<impl> _impl;
};

// private implementation

struct shared_file::impl {

    // in a multithreaded program
    // we require a condition variable to ensure that the shared resource is closed
    // when we try to re-open it (race condition)
    struct statics {
        std::mutex m;
        std::condition_variable cv;
        bool still_open = false;
        std::weak_ptr<impl> cache;
    };

    static statics& get_statics() {
        static statics _;
        return _;
    }

    impl() {
        std::cout << "opening file\n";
    }
    ~impl() {
        std::cout << "closing file\n";
        // close file here
        // and now that it's closed, we can signal the singleton state that it can be
        // reopened

        auto& stats = get_statics();

        // we *must* use a lock otherwise the compiler may re-order memory access
        // across the memory fence
        auto lock = std::unique_lock<std::mutex>(stats.m);
        stats.still_open = false;
        lock.unlock();
        stats.cv.notify_one();
    }
    void log(const std::string& s) {
        std::cout << "logging to file: " << s << std::endl;
    }
};

auto shared_file::get() -> std::shared_ptr<impl>
{
    auto& statics = impl::get_statics();

    auto lock = std::unique_lock<std::mutex>(statics.m);
    std::shared_ptr<impl> candidate;
    statics.cv.wait(lock, [&statics, &candidate] {
        return bool(candidate = statics.cache.lock())
        or not statics.still_open;
    });
    if (candidate)
        return candidate;

    statics.cache = candidate = std::make_shared<impl>();
    statics.still_open = true;
    return candidate;
}


// interface implementation

shared_file::shared_file() : _impl(get()) {}
void shared_file::log(const std::string& s) { _impl->log(s); }

// test our class
auto main() -> int
{
    using namespace std;

    auto j1 = jobbie();
    auto j2 = jobbie();
    auto j3 = jobbie();

    j1.log("doh");
    j2.log("ray");
    j3.log("me");

    {
        shared_file f;
        f.log("hello");
    }

    {
        shared_file().log("goodbye");
    }

    shared_file().log("here's another");

    shared_file f2;
    {
        shared_file().log("no need to reopen");
        shared_file().log("or here");
        shared_file().log("or even here");
    }
    f2.log("all done");


    return 0;
}

【讨论】:

    猜你喜欢
    • 2012-11-12
    • 2018-08-12
    • 2011-01-10
    • 1970-01-01
    • 1970-01-01
    • 2010-10-01
    • 1970-01-01
    相关资源
    最近更新 更多