C++ unordered_map emplace() 函数抛出段错误,我不知道为啥

Posted

技术标签:

【中文标题】C++ unordered_map emplace() 函数抛出段错误,我不知道为啥【英文标题】:C++ unordered_map emplace() function throwing seg fault and I have no idea whyC++ unordered_map emplace() 函数抛出段错误,我不知道为什么 【发布时间】:2019-12-16 06:50:46 【问题描述】:

我在使用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() 中调用.emplace() 时发生段错误。

应该发生的是addWord("Hi") 将映射添加到std::unordered_map&lt;std::string, Node&gt; 表。该映射应将 "Hi" 作为键值,并将 Node() 对象作为映射值。

这是第一个奇怪的部分:如果在第三次调用之前我只有一次调用 addWord(),则没有 seg 错误。这是输出:

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() 内部如何发生段错误。我能想到的唯一问题是我在addWord() 中创建Node() 的方式,但我看不出这将如何使addWord() 在前两个调用中成功,但在第三个调用中出现段错误。

我将非常感谢任何帮助!!!

【问题讨论】:

您需要向我们展示正确的minimal reproducible example,我们无法帮助您调试那些短行(尤其是因为错误很可能在其他地方)。也请花一些时间阅读how to ask good questions,以及this question checklist。 "我不知道发生了什么"" - 至少你可以看到导致问题的代码。把你自己放在我们的位置上。我们不知道是什么WordTable 真的看起来像,Node 真的看起来像什么。我们不是读心术的人。这需要一个正确的minimal reproducible example,它属于in your question。我的水晶球告诉我Node 在建造或破坏期间是邪恶的,没有好处(未初始化的table 成员盲目地delete's),但那是一个摇摆(wild-arse-guess),我的水晶球是如今,错误多于正确。 感谢两位的反馈!我现在将努力获得一个最小的可重现示例。 WordTable *kids; 是查看std::unique_ptr (unique_ptr&lt;WordTable&gt;) 的好地方。至于崩溃,它是双重删除,通常如果你写了一个析构函数,你想编写或阻止复制构造函数/操作符,副本与原始指针相同,因此删除两次. @manissss 我更完整地解释了编译器现在正在做什么以及如何进行复制/移动而不会使容器崩溃。 【参考方案1】:

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;

// 海合会 添加单词 1 开始就位 新孩子 0xa38c30 delete kids 0xa38c30 // 在 emplace 中被删除,但是当 `~WordTable` 稍后发生时(如果到了那么远)将被第二次删除,即存储在 WordTable 实例中的那个。 安顿成功 结尾加词 1 开始就位 new kids 0xa38c30 // 可以获得与 0xa38c30 现在“免费”相同的指针 删除孩子 0xa38c30 // 并再次删除 delete kids 0xa38c30 // 这次是两次,由于某些 GCC 特定的实现细节,当值“Hi”已经存在,因此不再需要该值时 安顿成功 结尾加词 2 添加单词 3 开始就位 new kids 0xa38cf0 // 再次使用相同的指针 SIGSEGV //这次没那么幸运,可能是因为上面的双重删除损坏了一些东西

您可以通过“删除”构造函数、操作符来防止默认复制:

Node(const Node &) = delete;
Node &operator = (const Node &) = delete;

这会将table.emplace(word, Node()); 变成一个编译错误,因为这是复制发生的地方。 尽管您调用了emplace,但您向它传递了一个完整的临时对象,因此它将尝试将其放置到复制构造函数Node(const Node &amp;)。 你想用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 // 这次做了一个新对象 delete kids 0xee8c30 // 删除原来的但 0xee8cd0 仍然有效 安顿成功 结尾加词 1 开始就位 新孩子 0xee8c30 复制构造函数 0xee8d90 删除孩子 0xee8d90 删除孩子 0xee8c30 安顿成功 结尾加词 2 添加单词 3 开始就位 新孩子 0xee8d40 复制构造函数 0xee8de0 删除孩子 0xee8d40 安顿成功 结尾加词 3 delete kids 0xee8cd0 // 当 main 返回时第一个副本被删除

WordTable 的副本很好,因为unordered_map&lt;string, Node&gt; 将使用刚刚提供的键/值单独复制每个键/值。

另一个类似的替代方法是提供合适的移动构造函数和运算符,与副本一起提供,或者在删除副本的情况下提供。

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;

添加单词 1 开始就位 new kids 0x1c4cc30 // 临时对象 移动构造函数 0x1c4cc30 // 最终的 emplace 值 delete kids 0 // 将其移出,因此这里没有删除任何内容 安顿成功 结尾加词 1 开始就位 新孩子 0x1c4ccf0 移动构造函数 0x1c4ccf0 delete kids 0x1c4ccf0 // 由于重复的“Hi”而删除 delete kids 0 // 它又被移动了,所以是空的 安顿成功 结尾加词 2 添加单词 3 开始就位 新孩子 0x1c4ccf0 移动构造函数 0x1c4ccf0 删除孩子 0 安顿成功 结尾加词 3 删除孩子 0x1c4cc30

请记住,无论您将移动对象置于何种状态(例如,此处为零 count 和空 kids)本身都必须是有效的。因此,您需要小心并有适当的if (kids == nullptr) 检查您是否这样做。


这样的案例对于std::unique_ptr 来说也是一个不错的案例,其中一些对象在一个唯一的地方被创建和销毁,从而节省了手动delete 的需要。它还会自动阻止默认复制,因为unique_ptr 本身不允许复制,但允许移动(注意:如果您有 ~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;
;

【讨论】:

非常感谢您的回答!如果理解正确,您是说 seg 错误是由于孩子指针的双重删除引起的未定义行为?【参考方案2】:

如果我们使用 Valgrind 编译和运行,我们会看到一些提示问题的编译器警告:

g++ -std=c++2a -fPIC -g -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds  -Weffc++       59351752.cpp    -o 59351752
59351752.cpp:23:10: warning: ‘struct WordTable::Node’ has pointer data members [-Weffc++]
   23 |   struct Node 
      |          ^~~~
59351752.cpp:23:10: warning:   but does not override ‘WordTable::Node(const WordTable::Node&)’ [-Weffc++]
59351752.cpp:23:10: warning:   or ‘operator=(const WordTable::Node&)’ [-Weffc++]

以及我们第一次使用后释放的指示:

valgrind --leak-check=full ./59351752   
==1137125== Memcheck, a memory error detector
==1137125== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1137125== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==1137125== Command: ./59351752
==1137125== 
add word 1
begin emplace
emplace succeeded
end add word 1
begin emplace
==1137125== Invalid read of size 8
==1137125==    at 0x10B3D8: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node> >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_M_begin() const (hashtable.h:384)
==1137125==    by 0x10AFD1: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node> >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::clear() (hashtable.h:2028)
==1137125==    by 0x10ACCD: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node> >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::~_Hashtable() (hashtable.h:1352)
==1137125==    by 0x10A905: std::unordered_map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, WordTable::Node, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node> > >::~unordered_map() (unordered_map.h:102)
==1137125==    by 0x10A94F: WordTable::~WordTable() (59351752.cpp:11)
==1137125==    by 0x10AAA3: WordTable::Node::~Node() (59351752.cpp:30)
==1137125==    by 0x10A9C5: WordTable::addWord(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int) (59351752.cpp:15)
==1137125==    by 0x10A3B2: main (59351752.cpp:52)
==1137125==  Address 0x4d7d228 is 24 bytes inside a block of size 64 free'd
==1137125==    at 0x483708B: operator delete(void*, unsigned long) (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==1137125==    by 0x10AAB0: WordTable::Node::~Node() (59351752.cpp:30)
==1137125==    by 0x10CBD9: std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>::~pair() (stl_pair.h:208)
==1137125==    by 0x10CC05: void __gnu_cxx::new_allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, true> >::destroy<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node> >(std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>*) (new_allocator.h:153)
==1137125==    by 0x10C58D: void std::allocator_traits<std::allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, true> > >::destroy<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node> >(std::allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, true> >&, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>*) (alloc_traits.h:497)
==1137125==    by 0x10BC32: std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, true> > >::_M_deallocate_node(std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, true>*) (hashtable_policy.h:2102)
==1137125==    by 0x10B548: std::pair<std::__detail::_Node_iterator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, false, true>, bool> std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node> >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_M_emplace<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, WordTable::Node>(std::integral_constant<bool, true>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, WordTable::Node&&) (hashtable.h:1655)
==1137125==    by 0x10B0B2: std::pair<std::__detail::_Node_iterator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, false, true>, bool> std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node> >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::emplace<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, WordTable::Node>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, WordTable::Node&&) (hashtable.h:749)
==1137125==    by 0x10AD2D: std::pair<std::__detail::_Node_iterator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node>, false, true>, bool> std::unordered_map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, WordTable::Node, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, WordTable::Node> > >::emplace<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, WordTable::Node>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, WordTable::Node&&) (unordered_map.h:388)
==1137125==    by 0x10A9B9: WordTable::addWord(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int) (59351752.cpp:15)
==1137125==    by 0x10A3B2: main (59351752.cpp:52)
==1137125==  Block was alloc'd at
==1137125==    at 0x4835DEF: operator new(unsigned long) (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==1137125==    by 0x10AA66: WordTable::Node::Node() (59351752.cpp:27)
==1137125==    by 0x10A9A6: WordTable::addWord(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int) (59351752.cpp:15)
==1137125==    by 0x10A3B2: main (59351752.cpp:52)
==1137125== 

这显然是由在编译器生成的复制构造函数中复制的Node 中的原始指针引起的。

如果我们简单地将Node::kids 更改为std::unique_ptr 并在离开main() 之前删除test,那么我们将获得一个干净的Valgrind 运行。

【讨论】:

以上是关于C++ unordered_map emplace() 函数抛出段错误,我不知道为啥的主要内容,如果未能解决你的问题,请参考以下文章

在C++里,emplace_back可以完全取代push_back吗?

C++ emplace_back()是什么

C++ std::set emplace 返回值 first second

在 C++ 地图中插入 vs emplace vs operator[]

C++的emplace_back函数介绍

C ++:堆栈的 push() 与 emplace() [重复]