【问题标题】:std::unordered_map::emplace object creationstd::unordered_map::emplace 对象创建
【发布时间】:2014-07-05 11:06:48
【问题描述】:

我正在选择将事物放入 unordered_map 的两种方法之一:

std::unordered_map<Key, Value> map;
map.emplace(
  std::piecewise_construct,
  std::forward_as_tuple(a),
  std::forward_as_tuple(b, c, d));

std::unordered_map<Key, DifferentValue> map;
auto& value = map[a];
if (value.isDefaultInitialized())
  value = DifferentValue(b, c, d);

我做了一些实验,看看哪一个表现更好,发现在插入独特元素时,行为(如效率)基本相同。

但是,在插入重复项的情况下,考虑到Value或DifferentialValue的构造并不是微不足道的,我惊讶地发现emplace构造对象不管是否插入。

因此,在这种情况下,第二种方法似乎更胜一筹,因为默认构造函数中只有 isDefaultInitialized_(true) ,仅此而已。

对于emplace,代码好像是:

... _M_emplace(std::true_type, _Args&&... __args) {
  __node_type* __node = _M_allocate_node(std::forward<_Args>(__args)...);
  const key_type& __k = this->_M_extract()(__node->_M_v);
  ...
  if (__node_type* __p = _M_find_node(__bkt, __k, __code)) {
     _M_deallocate_node(__node);
     return std::make_pair(iterator(__p), false);
  }
  return std::make_pair(_M_insert_unique_node(__bkt, __code, __node), true);
}

所以,虽然我将使用第二种方法(即使它需要移动赋值和移动构造函数和额外的字段),我想知道为什么 emplace 创建一个后来忽略的对象有很好的理由吗?也就是说,是否应该先检查是否需要创建对象,如果已经存在则提前退出?

(请注意,对于我的特殊情况,默认初始化项不被认为是有效的,所以问题实际上只是关于 emplace)

作为记录,我在 23.2.4 表 102 下找到了一些东西:

Effects: Inserts a value_type object t constructed with std::forward<Args>(args)...
if and only if there is no element in the container with key equivalent to the
key of t.

我认为这将允许不创建对象。

【问题讨论】:

  • 至少必须创建 key 才能使用 hashcomparison 函数。此问题已在 C++14 中为 std::map 解决。对于std::map,可以在不构造相应对象的情况下查找key。见en.cppreference.com/w/cpp/container/map/find。不幸的是,std::unordered_map 无法做到这一点。
  • 哦,有趣,我对此完全感兴趣。因此,为了获得键,它必须构造对象,所以与其说是操作顺序的选择,不如说是它必须创建它,以便甚至找出具有该键的对象是否已经存在?跨度>
  • 是的,它必须创建 key 才能确定对象是否已经存在。

标签: c++ c++11 unordered-map emplace


【解决方案1】:

在我看来,标准中引用的部分具有误导性,因为它表明只有在容器中没有匹配的元素时才构造对象。我猜他们是想说明:

效果:std::forward&lt;Args&gt;(args)... 构造一个value_type 对象t。当且仅当容器中不存在与t 的键等效的元素时,插入构造的对象t

原因是:函数emplace的实现必须构造t才能找出是否存在具有等效键的元素,因为实现必须调用hash函数和 equals 谓词。但是,通常只能使用 value_type 类型的对象调用它们,而不能使用用于构造这些对象的 元组

理论上,可以指定一个 emplace 函数,如果已经存在具有等效键的元素,则该函数不会构造 t。有趣的是,C++14 中会为std::map::find 添加类似的东西。请参阅以下文档:

有两个重载可以用于任意类型,只要 compare 函数满足一些额外的要求。有趣的是,std::unordered_map 没有这样的过载。

【讨论】:

  • 键是散列的,而不是值。那么如何构造一个值类型的对象来计算hash呢?
  • @haelix:valuekey mapped value 组成。 std::map::emplace是可变成员函数模板,参数和key之间没有直接的映射关系。因此,没有简单的方法可以让实现在不构造 value 的情况下访问 key
  • 我认为std::piecewise_construct 正是为了告诉哪些参数是关键。无论如何,我对此感到失望。似乎 unordered_map 不知道它实际上是一个键值映射 - 所以它不能只操作键?
  • 我也觉得 emplace 的这种行为很烦人。对我来说这是一个问题,因为第二次插入创建了对具有副作用的析构函数的调用。我认为这可以通过使用piecewise_construct 很容易解决,就像你说的那样。只需创建一个使用piecewise_construct(如pair的构造函数)的重载并使用第一个元组创建一个键,然后进行任何需要的散列和比较。这种方法的唯一缺点是,如果插入成功,您将同时调用键的构造函数和移动构造函数。
  • @dcmm88:不久前,我实际上实现了这样一个emplace 函数(以及相应的put 函数又名insert_or_update)作为概念证明(为我自己)。见pastebin.com/8nKZLMaC
【解决方案2】:

是的,std::unordered_map::emplace() 所做的第一件事是在搜索之前在内存中创建要放置的 KEY-VALUE 对,如果一个元素已经具有刚刚构造的 KEY存在于表中。如果找到这样的元素, emplace() 会立即再次销毁新创建的元素。这通常不是人们首先使用 emplace() 的原因,因为它是为了避免不必要的对象创建!

std::(unordered_)map::emplace() 的(恕我直言)破坏设计背后的原因可能是,首先创建 KEY 然后检查 KEY 是否存在的实现需要能够移动或者如果找不到 KEY,则将该 KEY 复制到 KEY-VALUE-pair 中的最终目的地。由于 emplace() 被添加到 STL 容器中,专门用于处理不可复制的不可移动对象,因此依赖于可移动/可复制 KEY 的 emplace 实现将是不完整的。

但是,所有合理的 KEY 中的 99% 要么是可复制构造的,要么是可移动构造的,或两者兼而有之,因此它们应该与 VALUE 分开处理,后者的构造可能要复杂得多。而对于 C++17 aka C++1z,语言之神对我们来说很好,并添加了 try_emplace() 方法:它的参数是对已经构造的 KEY 的引用,并且只需要构造相应的参数VALUE 到位。 try_emplace() 首先搜索 KEY。仅,如果 KEY 是新的,则通过复制或移动 KEY 并在适当位置构造 VALUE 来构造新的 KEY-VALUE 对。万岁!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多