C ++的std map插入语义的基本原理?

时间:2012-05-14 14:13:34

标签: c++ map standard-library

我对std::map::insert的语义感到有些困惑。我的意思是,我不是在抱怨 - 标准是标准,API就是这样。尽管如此,

insert

  

插入操作检查插入的每个元素是否   另一个元素已存在于具有相同键的容器中   value,如果是,则不插入元素,而不映射其映射值   以任何方式改变。

并且 - 仅在其单参数版本pair<iterator,bool> insert ( const value_type& x );中它甚至会告诉您它是否甚至将(新的,可能不同的)值插入到键中。据我所知,如果密钥已经存在,迭代器版本将默默地忽略插入。

对我来说,这只是反直觉,我希望在插入时覆盖值部分并将旧值部分丢弃。显然,STL的设计者有不同的想法 - 任何人都知道(历史)的基本原理,或者能够彻底解释现有语义如何(更有意义)?

以示例:

在单键映射中实现插入有几种基本方法,例如std::map

  • 插入,替换(如果已存在)
  • insert,如果已经存在则忽略(这是std :: map 的行为)
  • 插入,抛出错误(如果已存在)
  • 插入,UB(如果已存在)

我现在正在尝试理解为什么insert_or_ignoreinsert_or_replace(或insert_or_error)更有意义!


我查看了TC++PL的副本(不幸的是我只有德语版),有趣的是,Stroustrup在第17.4.1.7节( map 的列表操作)中写道:(对不起)来自德语的粗略翻译)

  

(...)通常,人们不关心一个键(sic!)是否是新的   在调用insert()(...)

之前已插入或已存在

在我看来,这只适用于 set ,而不适用于 map ,因为对于地图来说,如果提供的话确实会产生很大的不同插入了值或旧的值保留在地图中。 (这显然与密钥无关,因为密钥是等效的。)


注意:我了解operator[],我知道Effective STL的第24项以及提议的efficientAddOrUpdate功能。我只是对insert语义的基本原理感到好奇,因为我个人觉得它们反直觉。

5 个答案:

答案 0 :(得分:7)

插入方法不是你想要的,它听起来像......插入方法只是做名称所暗示的...插入值。我同意如果一个值尚未存在而创建一个值的能力,并且在某些情况下替换那里的值是很重要的,但在其他情况下,你真的宁愿不处理异常,返回值等等。只有在值尚未存在时才想进行插入。

这听起来像您正在寻找的方法(如上所述的BoBTFish)可能是[]运算符。只需使用它:

myMap["key"] = "value";

这将通过您的地图找到键“key”,并用“value”替换相应的值。如果密钥不在那里,它将创建它。这两种方法在不同的情况下非常有用,我发现自己只使用两种方法。

答案 1 :(得分:6)

我不知道官方的理由,但我会注意operator[]的双重性。

很明显,人们会喜欢这两种插入方式:

  • 纯添加剂
  • 添加剂/破坏性

如果我们将map视为数组的稀疏表示,那么operator[]的存在是有意义的。我不知道预先存在的词典是否存在并且已经确定了这种语法(也许,为什么不这样)。

此外,所有 STL容器都有几个insert的重载,这种接口的相似性允许通用编程。

因此,我们至少有两个API竞争者:operator[]insert

现在,在C ++中,如果您阅读:

array[4] = 5;

自然地,索引4处的单元格的内容已被破坏性地更新。因此,map::operator[]应该返回引用以允许此破坏性更新。

此时,我们现在也需要一个纯粹的加法版本,我们有这个insert方法。为什么不呢?

当然可以给insert operator[]提供与insert_or_ignore相同的语义,然后继续在顶部实现map方法 。这可能会有更多的工作。

因此,虽然我同意这可能会令人感到意外,但我认为我的推理并没有太大的缺陷,可能是我们在这里引导我们的情况的可能解释:)


关于您提出的替代方案:

  
      
  • 插入,UB(如果已存在)
  •   

幸运的是,它不是!

  
      
  • 插入,抛出错误(如果已存在)
  •   

只有Java(和派生词)才是异常疯狂的。 C ++是在特殊情况下使用异常的时候构思的。

  
      
  • 插入,替换(如果已存在)
  •   
  • insert,如果已经存在则忽略(这是std :: map的行为)
  •   

我们同意选择其中一个。请注意,即使{{1}}选择了第二个选项,它也不会完全忽略该项目已存在的事实,至少在单个项目版本中,因为它会警告您该项目不是插入

答案 2 :(得分:3)

我并不是说自己知道这个决定的原始理由,但要做出决定并不难。我想; - )

“插入或忽略”的当前行为使得实现其他两个行为变得非常容易 - 至少对于我们这些不是创建和使用非成员函数来补充标准库功能的人来说(“它是不是OOP-y够了!“)。

示例(现场编写,因此可能存在错误):

template<typename Map>
void insert_or_update(Map& map, typename Map::value_type const& x)
{
  std::pair<typename Map::iterator, bool> result = map.insert(x);
  if (!result.second)
    result.first->second = x.second; // or throw an exception (consider using
                                     // a different function name, though)
}

请注意,上面的函数与operator[]实际上并没有太大区别 - 是的,它避免了默认初始化,但同时(因为我很懒)它无法利用移动您的最新STL可能已经支持operator[]的语义。

无论如何,map的任何其他插入行为都会使实现其他行为更加繁琐,因为map::find仅在地图中尚未存在键时才返回结束哨兵。在<algorithm>(尤其是lower_bound)的帮助下,当然,仍然可以编写高性能附件函数而不会淹没它们的实现细节和丑陋的通用构造,例如循环; - )。

答案 3 :(得分:0)

insert()开始,您不希望触及容器中的现有对象。这就是为什么简单不触及它们。

答案 4 :(得分:0)

pair<iterator,bool>&lt; - bool部分不知道插入是否成功?

如果bool部分为false,则可以更新返回迭代器的值部分,以使用相同的键更新现有项目。