使用std::unordered_map::emplace()
时出现段错误。这是最小的可重现示例:
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
class WordTable {
public:
WordTable() {
total = 0;
}
~WordTable() {}
void addWord(const string word, const int incr = 1) {
cout << "begin emplace" << endl;
table.emplace(word, Node()); //this is where the seg fault occurs
cout << "emplace succeeded" << endl;
if (incr) {
table[word].incrementCount();
incrementTotal();
}
}
private:
struct Node {
public:
Node() {
count = 0;
kids = new WordTable();
}
~Node() {
delete kids;
}
int incrementCount() {
return ++count;
}
private:
int count;
WordTable* kids;
};
int incrementTotal() {
return ++total;
}
int total;
unordered_map<string, Node> table;
};
int main() {
WordTable tmp;
cout << "add word 1" << endl;
tmp.addWord("Hi");
cout << "end add word 1" << endl;
tmp.addWord("Hi");
cout << "end add word 2" << endl;
WordTable* test = new WordTable();
cout << "add word 3" << endl;
test->addWord("Hi");
cout << "end add word 3" << endl;
}
以及相应的输出:
add word 1
begin emplace
emplace succeeded
end add word 1
begin emplace
emplace succeeded
end add word 2
add word 3
begin emplace
Segmentation fault (core dumped)
段错误发生在对.emplace()
的第三次调用中对addWord()
的调用。
应该发生的是,addWord("Hi")
向std::unordered_map<std::string, Node>
表添加了映射。该映射应将"Hi"
作为键值,并将Node()
对象作为映射值。
这是第一个奇怪的部分:如果在第三个呼叫之前我只有一个呼叫addWord()
,则没有段错误。这是输出:
add word 1
begin emplace
emplace succeeded
end add word 1
end add word 2
add word 3
begin emplace
emplace succeeded
end add word 3
第二个怪异部分是,如果我静态分配test
,那么也就不会出现段错误。这是输出的内容:
add word 1
begin emplace
emplace succeeded
end add word 1
begin emplace
emplace succeeded
end add word 2
add word 3
begin emplace
emplace succeeded
end add word 3
我不知道发生了什么,也不知道为什么发生。我只是不了解STL unordered_map::emplace()
内部如何发生段错误。我能想到的唯一问题是我在Node()
中创建addWord()
的方式,但是我没有看到如何使addWord()
在前两个调用中成功,但是在第三个调用中出现段错误。
非常感谢您的协助!
在Node
中,您可以在构造函数和析构函数中分配并释放WordTable *kids
,但是它将具有default
Node(const Node &cp) // default : count(cp.count), kids(cp.kids) // no "new"! {}
当这些副本中的第一个副本被破坏时,指针将被删除,而其他副本则具有无效的指针,这有望在访问时崩溃(替代方法通常是某种形式的堆损坏)。在这种情况下,第二次访问发生在销毁[在我的系统上]副本之一时,在
main
的末尾,但是出于某种原因,您的编译器/设置可能会在emplace
本身中看到这种访问。您可以通过跟踪新内容/删除内容来看到此内容:Node() { count = 0; kids = new WordTable(); cout << "new kids " << kids << endl; } ~Node() { cout << "delete kids " << kids << endl; delete kids; }
// GCC加字1开始就位新孩子0xa38c30删除孩子0xa38c30 //在替换中被删除安置成功结束添加单词1开始就位新的孩子0xa38c30 //现在可以“免费”获得与0xa38c30相同的指针删除孩子0xa38c30 //并再次删除孩子0xa38c30 //这次是两次,由于某些GCC特定的实现细节安置成功结束添加单词2加字3开始就位新孩子0xa38cf0 //再次使用相同的指针SIGSEGV //这次没有那么幸运,可能是因为该重复删除仅破坏了某些内容
您可以通过“删除”构造函数,运算符来防止默认副本:
Node(const Node &) = delete; Node &operator = (const Node &) = delete;
这将使table.emplace(word, Node());
变成编译错误,因为这是复制发生的地方。尽管您调用了emplace
,但仍向它传递了一个完整的临时对象,因此它将尝试并放置到副本构造函数Node(const Node &)
中。您要对emplace
进行的操作是向其传递构造函数参数,这对于默认构造函数来说有点棘手,简单的table.emplace(word)
不会编译:
table.emplace(std::piecewise_construct, std::make_tuple(word), std::make_tuple());
或者,如果您希望对象可以安全地复制,请显式实现这些功能。
Node(const Node &cp) : count(cp.count), kids(new WordTable()) // create a full new object this time { *kids = *cp.kids; cout << "copy constructor " << kids << endl; } Node &operator = (const Node &cp) { *kids = *cp.kids; cout << "copy operator " << kids << endl; return *this; }
加字1开始就位新孩子0xee8c30复制构造函数0xee8cd0 //这次创建了一个新对象删除孩子0xee8c30 //删除原始的,但0xee8cd0仍然有效安置成功结束添加单词1开始就位新孩子0xee8c30复制构造函数0xee8d90删除孩子0xee8d90删除孩子0xee8c30安置成功结束添加单词2加字3开始就位新孩子0xee8d40复制构造函数0xee8de0删除孩子0xee8d40安置成功结束添加单词3delete kids 0xee8cd0 //主返回时删除第一个副本
WordTable
的副本很好,因为unordered_map<string, Node>
将使用刚刚提供的键/值分别复制每个键/值。对于
std::unique_ptr
,这种情况也是不错的选择,因为在单个唯一的位置创建和销毁了某些对象,从而节省了手动delete
的需要。它还会自动阻止默认复制,因为unique_ptr
本身不允许复制。
struct Node { public: Node() : count(0) , kids(std::make_unique<WordTable>()) // std::unique_ptr(new WordTable()) {} int incrementCount() { return ++count; } private: int count; std::unique_ptr<WordTable> kids; };