使用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)

seg错误发生在对.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,但是它将具有默认的复制构造函数和运算符。
这些将仅复制指针本身,而不创建新对象,例如:

Node(const Node &cp) // default
    : count(cp.count), kids(cp.kids) // no "new"!
{}

当这些副本中的第一个副本被销毁时,指针将被删除,而其他副本则具有无效的指针,这有望在访问时崩溃(替代方法通常是某种形式的堆损坏)。在这种情况下,第二次访问似乎因编译器而异,由于生成额外的副本,GCC似乎在emplace中遇到问题,MSVC直到主程序进入~WordTable()时才出现问题
返回(WordTable tmp堆栈变量)。

您可以通过跟踪新内容/删除内容来查看此内容:
Node() {
  count = 0;
  kids = new WordTable();
  cout << "new kids " << kids << endl;
}
~Node() {
  cout << "delete kids " << kids << endl;
  delete kids;
}
// GCC
add word 1
begin emplace
new kids 0xa38c30
delete kids 0xa38c30 // was deleted inside emplace, but when `~WordTable` happens later (if it got that far) will be deleted a second time, the one stored in the WordTable instance.
emplace succeeded
end add word 1
begin emplace
new kids 0xa38c30 // Can get the same pointer as 0xa38c30 is now "free"
delete kids 0xa38c30 // and again
delete kids 0xa38c30 // this time twice due to some GCC specific implementation detail, when the value "Hi" already existed so the value was no longer wanted
emplace succeeded
end add word 2
add word 3
begin emplace
new kids 0xa38cf0 // same pointer again
SIGSEGV // this time not so lucky, maybe because that double deletion just above corrupted something

You can prevent the default copy by "deleting" the constructor, operator:

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;
}
add word 1
begin emplace
new kids 0xee8c30
copy constructor 0xee8cd0 // this time made a new object
delete kids 0xee8c30 // deleted the original but 0xee8cd0 is still valid
emplace succeeded
end add word 1
begin emplace
new kids 0xee8c30
copy constructor 0xee8d90
delete kids 0xee8d90
delete kids 0xee8c30
emplace succeeded
end add word 2
add word 3
begin emplace
new kids 0xee8d40
copy constructor 0xee8de0
delete kids 0xee8d40
emplace succeeded
end add word 3
delete kids 0xee8cd0 // that first copy getting deleted when main returns

The copy of the WordTable is fine, as unordered_map<string, Node> will copy each key/value individually, using the ones just provided.

Another similar alternative would be to provide suitable move constructors and operators, either along side the copy ones, or with the copy deleted.

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;
}
add word 1
begin emplace
new kids 0x1c4cc30 // temporary object
move constructor 0x1c4cc30 // final emplace value
delete kids 0 // moved it out, so nothing is deleted here
emplace succeeded
end add word 1
begin emplace
new kids 0x1c4ccf0
move constructor 0x1c4ccf0
delete kids 0x1c4ccf0  // delete due to duplicate "Hi"
delete kids 0 // again it was moved, so empty
emplace succeeded
end add word 2
add word 3
begin emplace
new kids 0x1c4ccf0
move constructor 0x1c4ccf0
delete kids 0
emplace succeeded
end add word 3
delete kids 0x1c4cc30

Remember that whatever state you leave the moved object in (e.g. here zero count and null kids) needs to itself be valid. So you would need to be careful and have appropriate if (kids == nullptr) checks if you did that.


A case like this is also a good one for a std::unique_ptr, where some object is being created and destroyed within a single unique place, saving the need for a manual delete. It will also automatically prevent the default copy, as unique_ptr itself disallows copying, but allows moving (Note: If you have an a ~Node(), you won't get the move functions automatically).

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

关于c++ - C++ unordered_map emplace()函数抛出seg错误,我也不知道为什么,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/59351752/

10-10 21:27