为什么C++的std::map::operator[]不使用就地new?

5
如果您使用C++的std::map(以及其他容器)与值类型,您会注意到插入到映射中会调用您元素类型的析构函数。这是因为运算符[]的实现根据C++规范要求等同于以下内容:
(*((std::map<>::insert(std::make_pair(x, T()))).first)).second

它调用您类型的默认构造函数以构建该对。然后将该临时值复制到映射中,然后进行销毁。可以在此stackoverflow帖子这里在codeguru上中找到确认。我发现奇怪的是,这可以在不需要临时变量的情况下实现,并且仍然等效。C++有一个名为"inplace new"的功能。std::map和其他容器可以为对象分配空白空间,然后在分配的空间上显式调用元素的默认构造函数。 我的问题:为什么我看到的所有std::map的实现都没有使用原地new来优化这个操作?在我看来,这将显着提高此低级操作的性能。但是许多人已经研究了STL代码库,所以我认为必须有某些原因才会这样做。

测试用例包含在链接的stackoverflow帖子中。这是链接:https://dev59.com/IlHTa4cB1Zd3GeqPRGU7 - srm
Mike Seymour:所以你的意思是,在调试器关闭的情况下,这些额外的析构函数调用就会消失?你确定这是真的吗?即使是真的,这也意味着在开启调试时,执行起来更难调试(我无法在调试器中设置断点以寻找“真正”的范围问题)。这同时也会降低调试性能,尽管这只是一个较小的问题,但确实存在。出于这两个原因,对于这样一个低级别的库来说,使用就地新建似乎更为合适。 - srm
2个回答

4

通常情况下,用较低级别的操作来指定类似[]的高级别操作是一个好主意。

在C++11之前,如果要使用[]进行此操作,需要使用insert,这样做比较困难。

在C++11中,添加了std::map<?>::emplace和类似于std::pair的东西,使我们能够避免这个问题。如果重新定义它以使用这种就地构造方法,则额外的(希望省略的)对象创建将消失。

我想不出任何不这样做的理由。我鼓励您提议将其纳入标准化。

为了演示如何无需复制将元素插入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";
}

实时示例 - 如您所见,不会生成任何临时文件。

因此,如果我们替换

(*((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

不会创建任何临时文件(添加空格以便我可以跟踪())。


Mike Seymour之前的回答表明,程序员选择替代分配器的能力意味着临时对象的构造不能从代码中删除,并且必须在优化期间省略,这既从调试的角度来看是令人遗憾的,也从向新用户解释为什么在插入期间调用析构函数的角度来看是令人遗憾的。 - srm
@srm 当然可以。但是Mike错了。这只是标准中的一个小缺陷(效率低下),假设它还没有被修复(我没有检查最新版本)。分配器可以重新绑定,而emplace和piecewise构造可以让您在没有任何临时对象的情况下构造对象。默认构造可以通过空的“tuple”来完成。 - Yakk - Adam Nevraumont
我不太清楚,无法发表评论。这就是为什么我提出了问题。你能否在Mike的回答下发表你的评论,这样他就会收到通知,看看你们能否互相说服? - srm

0
首先,对于 std::mapoperator[<key>] 仅在未找到请求的 <key> 时等同于插入操作。在这种情况下,只需要引用键,并且只会生成对存储值的引用。
其次,在插入新元素时,无法知道是否会跟随复制操作。您可能有map[_k] = _v;,也可能有_v = map[_k];。后者当然具有与赋值外部相同的要求,即map[_k].method_call();,但不使用复制构造函数(没有源可构造)。关于插入,所有上述操作都要求调用value_type的默认构造函数并为其分配空间。即使我们在编写operator[]时可以知道我们处于赋值用例中,由于操作顺序,我们也不能使用“就地新建”。必须先调用value_type构造函数,然后是value_type::operator=,这需要调用复制构造函数。
不过,思路很好。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接