C++ unordered_map emplace() 函数抛出 seg 错误,我不知道为什么

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()在前两个调用中成功,但在第三个调用中出现段错误。

我将非常感谢任何帮助!

Fir*_*cer 5

在 中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)