就地初始化风险

In-place initialization risks

本文关键字:初始化      更新时间:2023-10-16

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

所以我一直在考虑一种就地初始化器。类似的东西

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;
}

这样,T的构造函数中的代码可以在映射中查找它,即使它还没有完全构造好。

这种设计固有的风险和问题是什么?到目前为止,我已经确定了异常安全性,unique_ptr的析构函数将很尴尬,以及访问半构建的T的风险。

编辑:

粗略地说,T表示图中的一个节点,它绝对不是非循环的,也永远不会是非循环的。在T的构造函数中,为了计算关于T的一些内容,我想看看T的子节点——它可以包含对该T实例的直接引用。想象一下类似的东西

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的实例字段,则可以将它们分组在由initialize分配的具有const字段的非常数struct字段中。你甚至可以定义某种形式的optional,它永远不能被分配不止一次,但可能这太过分了。

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

  1. unique_ptr的析构函数将在未分配new的指针上调用delete,这是未定义的行为。您应该使用带有专用/noop解除定位器的unique_ptr,除非您可以控制所有可能从映射中删除该内容的代码,并确保它使用与您在catch子句中使用的相同的release-erase-free样板
  2. unique_ptr也适用于不完整的类型,因此它不会尝试访问您的对象,并且您的T*在函数中是隔离的。因此,只有在以下情况下,才能访问部分构造的T
    • T的构造函数本身泄漏了一个引用(这将独立于此处的代码),或者
    • 其他线程试图查找新项目。如果您的地图也是函数的本地地图,则情况似乎并非如此。如果您正在执行多线程,则必须更改设计,除非性能不重要。因为这里唯一要做的就是在映射上创建一个可重入的互斥锁,这将破坏您从多线程中获得的所有好处

编辑

响应您的编辑。首先,我觉得它很干净。看起来你并没有在做什么奇怪的事情。但是你必须处理析构函数的问题,因为你的映射最终会被破坏(程序终止也会导致析构函数被调用)。

总之:

  1. 我觉得你用钥匙的方式很奇怪。如果密钥本身已经包含关于子节点的信息,为什么还要在节点本身中复制这些信息?信息重复会导致数据不同步错误
  2. 难道你不能在查找之前更改T构造函数来检查subkey,并避免查找它自己的key