man*_*sss 1 c++ unordered-map segmentation-fault
我在使用时遇到段错误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;
}
Run Code Online (Sandbox Code Playgroud)
以及相应的输出:
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)
Run Code Online (Sandbox Code Playgroud)
seg 错误发生在.emplace()第三次调用 to 的调用中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
Run Code Online (Sandbox Code Playgroud)
第二个奇怪的部分是,如果我静态分配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
Run Code Online (Sandbox Code Playgroud)
我不知道发生了什么,也不知道为什么会发生。我根本不明白 STL 内部如何发生段错误unordered_map::emplace()。Node()我能想到的唯一问题是我创建in 的方式addWord(),但我不知道这将如何addWord()在前两个调用中成功,但在第三个调用中出现段错误。
我将非常感谢任何帮助!
在 中Node,您在构造函数和析构函数中分配和释放WordTable *kids,但它将具有默认的复制构造函数和运算符。这些只会复制指针本身,而不创建新对象,例如:
Node(const Node &cp) // default
: count(cp.count), kids(cp.kids) // no "new"!
{}
Run Code Online (Sandbox Code Playgroud)
当这些副本中的第一个被破坏时,指针将被删除,从而为其他副本留下无效指针,这将有望在访问时崩溃(替代方案通常是某种形式的堆损坏)。在这种情况下,第二次访问似乎因编译器而异,GCC 似乎由于制作额外的副本而遇到问题emplace,MSVC 直到~WordTable()main 返回时(WordTable tmp堆栈变量)才会遇到问题。
您可以通过跟踪新建/删除来看到这一点:
Node() {
count = 0;
kids = new WordTable();
cout << "new kids " << kids << endl;
}
~Node() {
cout << "delete kids " << kids << endl;
delete kids;
}
Run Code Online (Sandbox Code Playgroud)
// 海湾合作委员会 添加单词1 开始就位 新孩子0xa38c30 delete kids 0xa38c30 // 在 emplace 内被删除,但是当 `~WordTable` 稍后发生时(如果到了那一步)将被第二次删除,即存储在 WordTable 实例中的那个。 安置成功 末尾添加单词1 开始就位 new kids 0xa38c30 // 可以获得与 0xa38c30 相同的指针,现在“免费” // 再次删除孩子 0xa38c30 删除 kids 0xa38c30 // 这次两次,由于一些 GCC 特定的实现细节,当值“Hi”已经存在时,不再需要该值 安置成功 末尾添加单词2 添加单词3 开始就位 new kids 0xa38cf0 // 再次相同的指针 SIGSEGV // 这次没那么幸运,可能是因为上面的双重删除损坏了某些东西
您可以通过“删除”构造函数、运算符来防止默认复制:
Node(const Node &) = delete;
Node &operator = (const Node &) = delete;
Run Code Online (Sandbox Code Playgroud)
这将变成table.emplace(word, Node());编译错误,因为这是复制发生的地方。尽管您调用了emplace,但您向它传递了一个完整的临时对象,因此它将尝试放置到复制构造函数中Node(const Node &)。您想要做的emplace是将构造函数参数传递给它,这对于默认构造函数来说有点棘手,简单的table.emplace(word)将无法编译:
table.emplace(std::piecewise_construct, std::make_tuple(word), std::make_tuple());
Run Code Online (Sandbox Code Playgroud)
或者,如果您希望对象可以安全地复制,请显式实现这些函数。
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;
}
Run Code Online (Sandbox Code Playgroud)
添加单词1 开始就位 新孩子 0xee8c30 复制构造函数 0xee8cd0 // 这次创建了一个新对象 delete kids 0xee8c30 // 删除了原来的但0xee8cd0仍然有效 安置成功 末尾添加单词1 开始就位 新孩子 0xee8c30 复制构造函数 0xee8d90 删除孩子0xee8d90 删除孩子0xee8c30 安置成功 末尾添加单词2 添加单词3 开始就位 新孩子 0xee8d40 复制构造函数 0xee8de0 删除孩子0xee8d40 安置成功 末尾添加单词3 delete kids 0xee8cd0 // 当 main 返回时第一个副本被删除
的副本WordTable很好,因为unordered_map<string, Node>将使用刚刚提供的键/值单独复制每个键/值。
另一种类似的替代方案是提供合适的移动构造函数和运算符,可以与复制的构造函数和运算符一起提供,也可以删除副本。
Node(const Node &cp) = delete;
Node &operator = (const Node &cp) = delete;
Node(Node && mv)
: count(mv.count), kids(mv.kids)
{
mv.kids = nullptr; // took kids away from mv
cout << "move constructor " << kids << endl;
}
Node &operator = (Node &&mv)
{
swap(count, mv.count);
swap(kids, mv.kids);
cout << "move operator " << kids << " from " << mv.kids << endl;
return *this;
}
Run Code Online (Sandbox Code Playgroud)
添加单词1 开始就位 new kids 0x1c4cc30 // 临时对象 move构造函数0x1c4cc30 //最终的emplace值 delete kids 0 // 将其移出,因此此处没有删除任何内容 安置成功 末尾添加单词1 开始就位 新孩子 0x1c4ccf0 移动构造函数 0x1c4ccf0 删除孩子 0x1c4ccf0 // 由于重复“Hi”而删除 delete kids 0 // 再次被移动,所以是空的 安置成功 末尾添加单词2 添加单词3 开始就位 新孩子 0x1c4ccf0 移动构造函数 0x1c4ccf0 删除孩子 0 安置成功 末尾添加单词3 删除孩子0x1c4cc30
请记住,无论您将移动的对象保留在什么状态(例如此处的 0count和 null kids),其本身都需要有效。if (kids == nullptr)因此,如果您这样做,您需要小心并进行适当的检查。
像这样的情况对于 a 来说也是一个很好的情况std::unique_ptr,其中某个对象在一个唯一的位置中创建和销毁,从而节省了对手册的需要delete。它还会自动阻止默认复制,因为unique_ptr它本身不允许复制,但允许移动(注意:如果您有 a ~Node(),则不会自动获得移动功能)。
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;
};
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
2136 次 |
| 最近记录: |