【问题标题】:Is it possible to ensure that a function is only called during the 'static initialization' step是否可以确保仅在“静态初始化”步骤期间调用函数
【发布时间】:2018-12-19 09:58:53
【问题描述】:

我想知道是否可以确保仅在程序的静态初始化步骤中调用函数?

例如,假设我有一些单例类,它包含一个 std::map 对象并公开了它的 insertat 方法。我想确保从中读取数据(at 方法)是线程安全的,据我了解,这需要确保没有任何东西在修改数据(即使用 insert 方法)。

地图打算在静态初始化期间被填充,此时我假设只有一个线程。一旦main() 开始,我有什么方法可以确保没有误导用户调用insert


示例代码

#include <map>
#include <string>

class Singleton {
  private:
    std::map<std::string, std::string> m_map;
  public:
    static Singleton& instance() {
      static Singleton theSingleton;
      return theSingleton;
    }
    static bool insert(const std::string& key, const std::string& value) {
      return instance().m_map.insert(std::make_pair(key, value) ).second;
    }
    static std::string at(const std::string& key) {
      return instance().m_map.at(key);
    }
};

static bool inserted = Singleton::insert("Hello", "World"); // fine

bool addItem(const std::string& key, const std::string& value) {
  return Singleton::insert(key, value); // not OK
}

不用说(?)实际代码比这个简单的例子要复杂得多。


解决方案后编辑:看起来尽可能安全的最佳方法是维护一个status 变量,该变量记录单例是处于“插入”还是“读取”模式并采取相应行动。感谢大家的意见和建议!

【问题讨论】:

  • 如何让 insert 成为一个用构造函数调用的私有函数并使映射为 const?
  • @AdamZahran 原则上应该允许其他翻译单位向地图添加信息(只要他们在 main 之前添加)。
  • 这似乎不是一个很好的设计选择,因为在这种情况下地图内容有点不可预测,例如如果在两个不同的编译单元中使用相同的键插入两个不同的值,哪一个最终会出现在 map 中?
  • 你会在地图完全初始化之前 *read 吗?换句话说,at 方法会在 main 之前被调用吗?
  • @liliscent at 不应在 main 之前调用(或者至少我看不出有任何理由 + 如果有必要,我可以将此作为要求)。我可以保证没有insert 调用应该依赖于任何at 调用。

标签: c++ c++11 static-initialization


【解决方案1】:

我猜您还想在设置应用程序时使用“at”方法。 为什么不添加一个“锁定”方法并在主函数中调用那个简单的函数呢?

#include <map>
#include <string>

class Singleton {
private:
    std::map<std::string, std::string> m_map;
    bool m_locked;
    Singleton() : m_locked(false) { }

public:
    static Singleton& instance() {
        static Singleton theSingleton;
        return theSingleton;
    }

    static void lock() {
        instance().m_locked = true;
    }

    static bool insert(const std::string& key, const std::string& value) {
        if (instance().m_locked) { return false; }
        return instance().m_map.insert(std::make_pair(key, value)).second;
    }
    static std::string at(const std::string& key) {
        return instance().m_map.at(key);
    }
};

static bool inserted = Singleton::insert("Hello", "World"); // fine

bool addItem(const std::string& key, const std::string& value) {
    return Singleton::insert(key, value); // not OK
}

int main(int argc, char** argv)
{
    Singleton::lock();
    Singleton::insert("Hello2", "World2"); // fails
    return 0;
}

【讨论】:

    【解决方案2】:

    如果你能保证用户在初始化阶段main()之前不会读到map,一种解决办法是构造一个静态map只做初始化,然后在构造singleton的时候把它移到singleton中。

    由于构造是在第一次调用instance() 时发生的,因此您可以确定映射已正确初始化。

    那么对insert 的其他调用将不会对单例产生影响。您还可以添加互斥锁来保护insert 以避免 UB 处于竞争状态。

    class Singleton {
      private:
        std::map<std::string, std::string> m_map;
        static auto& init_map() {
            static std::map<std::string, std::string> m;
            return m;
        }
        Singleton() {
            m_map = std::move(init_map());
            init_map().clear();
        }
      public:
        static Singleton& instance() {
          static Singleton theSingleton;
          return theSingleton;
        }
        static bool insert(const std::string& key, const std::string& value) {
          // you can also add mutex to protect here,
          // because calling insert from different threads without
          // protection will screw up its internal state, even if
          // the init_map becomes useless after main
          return init_map().insert(std::make_pair(key, value) ).second;
        }
        static std::string at(const std::string& key) {
          return instance().m_map.at(key);
        }
    };
    

    【讨论】:

    • 静态初始化过程中会不会出现竞态(即此时可以有多个线程)?
    • @extiam 从我阅读标准的方式来看,在执行 main() 之前创建另一个线程是合法的(即从静态初始化程序中)。如果有人这样做,我会质疑设计的智慧。
    • 保护起来并不昂贵,尽管它可能是不明智的。我没有预料到会有大量的插入调用,所以添加互斥体并不痛苦。
    • @Oliv 这是真的。将它作为静态局部变量放在另一个函数中,其余相同。
    【解决方案3】:

    就像 Jürgen 使用非 Java 方式但 c/c++ 方式创建单例(即命名空间)。

    为什么要这样做

    • 在链接时优化更高效,更容易,因为无需取消引用 this 即可访问状态;
    • 无需维护代码以确保唯一性:唯一性由链接器确保。

    singleton.hpp

    namespace singleton{
      void lock();
      bool instert(const std::string& key, const std::string& value);
      std::string at(const std::string& key)
      }
    

    singleton.cpp

    namespace singleton{
      namespace{
        inline decltype(auto) 
        get_map(){
          static std::map<std::string, std::string> m_map;
          return m_map;
          }
        bool m_locked=false; //OK guarenteed to happen before any dynamic initialization [basic.start.static]
        }
    
      void lock() {
        m_locked = true;
        }
    
      bool insert(const std::string& key, const std::string& value) {
        if (m_locked) { return false; }
        return get_map().insert(std::make_pair(key, value)).second;
        }
    
      std::string at(const std::string& key) {
        return get_map().at(key);
        }
      }
    

    此外,如果必须在通用代码中使用单例,则可以使用结构体:

    struct singleton_type{
      static void lock() {singleton::lock();}
      static auto insert(const std::string& key, const std::string& value) {
          return singleton::insert(key,value);
          }
      static auto at(const std::string& key) {
          return singleton::at(key,value);
          }
      };
    auto x = singleton_type{};
    auto y = singleton_type{}; 
    // x and y refers to the same and unique object file!!!
    

    明天,停止用 java 编码:)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-10-03
      • 1970-01-01
      • 1970-01-01
      • 2016-11-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多