C++ 就地初始化风险

C++ 就地初始化风险,c++,C++,目前,我有一个设计,通过一个键映射对象,在一个无序的映射中。问题是,在这个对象的构造函数中,我需要按键查找它——即使它还不存在。到目前为止,我已经通过推迟一切解决了这个问题,但这相当尴尬 所以我一直在考虑一种就地初始化器。差不多 std::unordered_map<K, std::unique_ptr<T, FunkyDeleter>> map; T* ptr = malloc(sizeof(T)); map[key] = std::unique_ptr<T, F

目前,我有一个设计,通过一个键映射对象,在一个
无序的
映射中。问题是,在这个对象的构造函数中,我需要按键查找它——即使它还不存在。到目前为止,我已经通过推迟一切解决了这个问题,但这相当尴尬

所以我一直在考虑一种就地初始化器。差不多

std::unordered_map<K, std::unique_ptr<T, FunkyDeleter>> map;
T* ptr = malloc(sizeof(T));
map[key] = std::unique_ptr<T, FunkyDeleter>(ptr);
try {
    new (ptr) T(args);
} catch(...) {
    map[key].release();
    map.erase(key);
    free(ptr);
    throw;
}
struct K {
    std::vector<K*> subkeys;
};
class T {
    std::vector<T*> child_nodes;
public:
    T(K* key, graph& graph) {
        for(auto subkey : keys->subkeys)
            child_nodes.push_back(graph.get(subkey));
    }
    std::vector<T*> children() { return child_nodes; }
};
class graph {
    std::unordered_map<K*, std::unique_ptr<T>> nodes;
public:
    T* get(K* key) {
        if (nodes.find(key) == nodes.end())
            nodes[key] = std::unique_ptr<T>(new T(key, *this));
        return nodes[key].get();
    }
};
int main() {
    graph g;
    K key1;
    K key2;
    key1.subkeys.push_back(&key2);
    key2.subkeys.push_back(&key1);
    g.get(&key1);
}

这显然不适用于K对象中的循环引用。问题是我将如何支持他们。到目前为止,我只是推迟了所有的工作,这样
T
就不会评估构造函数中任何潜在的引用代码,但这在某些地方会导致一些非常尴尬的设计。我想尝试在构建时将指向T的指针放入映射中,以便循环引用在构造函数中正确计算,我可以扔掉这个延迟的工作,因为其中一些实际上有重要的副作用(由于第三方设计,我无法避免)管理延迟的副作用是一件棘手的事情。

当你有如此紧密的依赖关系时,通常构造函数和析构函数很容易变得一团糟

一般来说,你应该避免这样的事情。有时,在无效状态下构造对象,然后使用
initialize
方法初始化它们,这更为清晰。如果您有一些希望成为
const
的实例字段,则可以将它们分组到非const
struct
字段中,其中包含
const
字段,该字段由
initialize
指定。您甚至可以定义某种形式的可选,它永远不能被分配超过一次,但这可能有点过头了

回到问题上来,我看到的是:

  • unique\u ptr
    的析构函数将对未分配
    new
    的指针调用
    delete
    ,这是未定义的行为。您应该使用带有专用/noop deallocator的唯一\u ptr,除非您可以控制可能从映射中删除该内容的所有代码,并确保它使用与您在
    catch
    子句中使用的相同的
    release erase free
    样板文件
  • unique\u ptr
    也适用于不完整的类型,因此它不会尝试访问您的对象,并且您的
    T*
    被隔离在函数内部。因此,只有在以下情况下,才能访问部分构造的
    T
    • T
      的构造函数本身泄漏引用(这与此处的代码无关),或者
    • 其他线程试图查找新项目。如果您的映射也是函数的本地映射,则情况似乎并非如此。如果你在做多线程,那么你必须改变你的设计,除非性能不重要。因为这里唯一要做的就是在映射上使用可重入的互斥锁,这将破坏您从多线程中获得的所有好处
  • 编辑 回应您的编辑。首先,我觉得它很干净。看起来你没有做什么奇怪的事。但您必须处理析构函数问题,因为您的映射最终将被销毁(程序终止也会导致调用析构函数)

    无论如何:

  • 我觉得你用钥匙的方式很奇怪。如果密钥本身已经包含有关子节点的信息,为什么还要在节点本身中复制该信息?信息重复会导致数据不同步错误
  • 在查找之前,您是否可以更改
    t
    构造函数以检查
    子项
    ,并避免查找它自己的

  • 你能提供更多的背景吗?上述代码位于何处,各种参数是什么?
    地图的范围是什么?目前,在我看来,简单地使用默认构造的
    std::unique_ptr
    将是一个简单的解决方案,但我不确定我是否完全理解您的情况。“在这个对象的构造函数中,我需要按键查找它”您可能有一个很好的理由支持此声明。你介意详细说明这一部分吗?其中一个是UB,它将
    std::unique齌ptr
    (它将
    delete
    )与
    malloc
    (它的挂件是
    free
    )混合在一起
    map[key]
    可以在
    std::unique_ptr(ptr)
    之后执行。我想您应该提供一个定制的deleter,它实际上执行
    p->~T();自由基(p)。但是如果
    map[key]
    在对
    std::unique\u ptr(ptr)
    求值后抛出异常(例如OOM),您将得到UB。这就是你在最后一段中描述的“用于
    unique\u ptr
    的析构函数会很尴尬”吗?当然,这种情况可以通过使用
    auto&uptr=map[key];uptr=std::unique_ptr(ptr)@dyp:我不太关心OOM。我只是想说我知道我必须为unique_ptr提供一个自定义的deleter,并且准确地管理对象何时被删除将是一个问题。至于循环查找,T的工作基本上需要支持循环查找。唯一的问题是我是否可以将这些工作的一部分转移到构造函数中。我目前也不支持图中的并发性,如果我支持的话,它将涉及序列化这一方面,因为图的语义要求每个查找返回相同的T。数据只有在可以变异的情况下才能变得不同步,而输入不能。这并不是真正的重复,因为查找可能要复杂得多——图形的要点是以更有用的方式表示输入。在查找之前,我无法真正检查这一点,因为有许多方法可以引用同一对象。确定键对应于哪个节点的唯一方法是尝试查找它,到那时,已经太晚了。bug很有趣,因为它们与