就地初始化风险
In-place initialization risks
目前,我有一个通过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
,它永远不能被分配不止一次,但可能这太过分了。
回到问题上来,我看到的是:
unique_ptr
的析构函数将在未分配new
的指针上调用delete
,这是未定义的行为。您应该使用带有专用/noop解除定位器的unique_ptr,除非您可以控制所有可能从映射中删除该内容的代码,并确保它使用与您在catch
子句中使用的相同的release-erase-free
样板unique_ptr
也适用于不完整的类型,因此它不会尝试访问您的对象,并且您的T*
在函数中是隔离的。因此,只有在以下情况下,才能访问部分构造的T
:T
的构造函数本身泄漏了一个引用(这将独立于此处的代码),或者- 其他线程试图查找新项目。如果您的地图也是函数的本地地图,则情况似乎并非如此。如果您正在执行多线程,则必须更改设计,除非性能不重要。因为这里唯一要做的就是在映射上创建一个可重入的互斥锁,这将破坏您从多线程中获得的所有好处
编辑
响应您的编辑。首先,我觉得它很干净。看起来你并没有在做什么奇怪的事情。但是你必须处理析构函数的问题,因为你的映射最终会被破坏(程序终止也会导致析构函数被调用)。
总之:
- 我觉得你用钥匙的方式很奇怪。如果密钥本身已经包含关于子节点的信息,为什么还要在节点本身中复制这些信息?信息重复会导致数据不同步错误
- 难道你不能在查找之前更改
T
构造函数来检查subkey
,并避免查找它自己的key
吗
- 是否可以初始化不可复制类型的成员变量(或基类)
- C++使用整数的压缩数组初始化对象
- C++初始化基类
- 多成员Constexpr结构初始化
- 复制列表初始化的隐式转换的等级是多少
- 内联映射初始化的动态atexit析构函数崩溃
- 如何在C++中初始化嵌套类中的2个memeber
- 如何声明特征矩阵,然后通过嵌套循环初始化它
- 没有用于初始化C++中的变量模板的匹配构造函数
- 在未初始化映射的情况下,将值插入到映射的映射中
- C++成员初始化
- 为什么在C++中首先初始化成员类
- 同时具有"聚合初始化"和"模板推导"
- 初始化具有非默认构造函数的std::数组项的更好方法
- 是否可以在编译时初始化数组,以便在运行时不会花费时间?
- 我可以使用条件运算符初始化C风格的字符串文字吗
- 在C和C++中初始化结构中的数组
- 标准是否使用多余的大括号(例如 T{{{10}}})定义列表初始化?
- 在函数内部的声明中初始化数组,并在外部使用它
- 继承:构造函数,初始化C++11中基类的类C数组成员