【问题标题】:Why doesn't C++ std::map::operator[] use inplace new?为什么 C++ std::map::operator[] 不使用就地新的?
【发布时间】:2014-09-24 15:42:23
【问题描述】:

如果您将 C++ std::map(和其他容器)与值类型一起使用,您会注意到插入映射会调用元素类型的析构函数。这是因为 C++ 规范要求运算符 [] 的实现等价于

(*((std::map<>::insert(std::make_pair(x, T()))).first)).second

它会调用您类型的默认构造函数来构建该对。然后将该临时值复制到映射中,然后销毁。可以在this stackoverflow posthere on codeguru 中找到对此的确认。

我觉得奇怪的是,这可以在不需要临时变量的情况下实现,并且仍然是等效的。 C++ 有一个特性叫做"inplace new"。 std::map 和其他容器可以为对象分配空间,然后在分配的空间上显式调用元素的默认构造函数。

我的问题:为什么我见过的 std::map 的实现都没有使用 inplace new 来优化这个操作?在我看来,这将大大提高这种低级操作的性能。但是很多人都研究过STL代码库,所以我认为这样做一定有一些原因。

【问题讨论】:

  • 测试用例包含在链接的 stackoverflow 帖子中。这是链接:stackoverflow.com/questions/4017892/…
  • Mike Seymour:所以你的意思是关闭调试器后,这些对析构函数的额外调用就会消失?你知道那是真的吗?即使这是真的,这确实意味着在调试的情况下,执行更难调试(我不能只在调试器中设置断点来寻找“真正的”范围问题)。调试时它的性能也较低,这是一个较小的问题,但仍然是真实的。在我看来,这两个原因都是使用 inplace new 而不是这样一个低级库的原因。

标签: c++ stl new-operator


【解决方案1】:

一般来说,您可以在较低级别的操作中指定较高级别的操作,例如 []

在 C++11 之前,如果不使用 insert,使用 [] 会很困难。

在 C++11 中,为 std::pair 添加 std::map&lt;?&gt;::emplace 和类似的东西使我们能够避免这个问题。如果您重新定义它并使用这种就地构造,那么额外的(希望被省略的)对象创建将会消失。

我想不出不这样做的理由。我鼓励您提出标准化建议。

为了演示在std::map 中的无副本插入,我们可以执行以下操作:

#include <map>
#include <iostream>

struct no_copy_type {
  no_copy_type(no_copy_type const&)=delete;
  no_copy_type(double) {}
  ~no_copy_type() { std::cout << "destroyed\n"; }
};
int main() {
  std::map< int, no_copy_type > m;
  m.emplace(
    std::piecewise_construct,
    std::forward_as_tuple(1),
    std::forward_as_tuple(3.14)
  );
  std::cout << "destroy happens next:\n";
}

live example -- 如您所见,没有生成临时文件。

如果我们替换

(*((std::map<>::insert(std::make_pair(x, T()))).first)).second

(*
  (
    (
      std::map<>::emplace(
        std::piecewise_construct,
        std::forward_as_tuple(std::forward<X>(x)),
        std::forward_as_tuple()
    )
  ).first
).second

不会创建临时的(添加空格以便我可以跟踪()s)。

【讨论】:

  • Mike Seymour 早些时候的回答表明,程序员选择备用分配器的能力意味着临时的构造不能从代码中删除,并且必须在优化过程中被省略,这对双方来说都是可悲的从调试的角度,向新用户解释为什么在插入期间调用析构函数。
  • @srm 当然。但迈克错了。这是标准中的一个小缺陷(效率低下),假设它尚未修复(我没有检查最新的)。分配器是可以反弹的,安放和分段构造可以让你在没有任何临时性的情况下构造对象。默认构造可以通过一个空的tuple来完成。
  • 我知道的不够多,无法发表评论。这就是我问这个问题的原因。您能否在 Mike 的回答上发表您的评论,以便他收到通知,看看你们两个能否说服对方?
【解决方案2】:

首先,std::mapoperator[&lt;key&gt;] 仅相当于插入操作如果请求的&lt;key&gt; 未找到。在这种情况下,只需要对 key 的引用,并且只需要对存储的 value 的引用。

其次,当插入新元素时,无法知道是否会进行复制操作。你可能有map[_k] = _v;,或者你可能有_v = map[_k];。后者当然与赋值之外的要求相同,即map[_k].method_call();,但使用复制构造函数(没有可构造的源)。关于插入,上述所有要求都需要调用value_type 的默认构造函数并为其分配空间。即使我们在编写operator[] 时可以知道我们在赋值用例中,但由于操作顺序,我们不能使用“inplace new”。必须首先调用 value_type 构造函数,然后调用 value_type::operator=,这需要调用复制构造函数。

虽然想法不错。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-03-16
    • 2017-02-27
    • 1970-01-01
    • 2016-08-30
    • 1970-01-01
    • 2022-08-24
    相关资源
    最近更新 更多