如果键不存在,为啥 std::map operator[] 会创建一个对象?

Posted

技术标签:

【中文标题】如果键不存在,为啥 std::map operator[] 会创建一个对象?【英文标题】:Why does std::map operator[] create an object if the key doesn't exist?如果键不存在,为什么 std::map operator[] 会创建一个对象? 【发布时间】:2009-10-28 19:30:22 【问题描述】:

我很确定我已经在某个地方看到了这个问题(comp.lang.c++?Google 似乎也没有在那里找到它)但是在这里快速搜索似乎没有找到它所以这里是:

如果键不存在,为什么 std::map operator[] 会创建一个对象?我不知道,但对我来说,如果你与大多数其他 operator[](如 std::vector)相比,这似乎违反直觉,如果你使用它,你必须确保索引存在。我想知道在 std::map 中实现这种行为的理由是什么。就像我说的那样,当使用无效键访问时,更像是向量中的索引并崩溃(我猜是未定义的行为)不是更直观吗?

看到答案后细化我的问题:

好的,到目前为止,我得到了很多答案,基本上它很便宜,所以为什么不或类似的东西。我完全同意这一点,但为什么不为此使用专用函数(我认为其中一条评论说在 java 中没有 operator[] 并且该函数称为 put)?我的观点是为什么 map operator[] 不像矢量那样工作?如果我在向量的超出范围索引上使用 operator[],我不希望它插入一个元素 即使它很便宜,因为这可能意味着我的代码中有错误。我的观点是为什么它与地图不一样。我的意思是,对我来说,在地图上使用 operator[] 意味着:我知道这个密钥已经存在(无论出于何种原因,我只是插入了它,我在某处有冗余,无论如何)。我认为这样会更直观。

也就是说,使用 operator[] 执行当前行为有什么好处(仅为此,我同意应该存在具有当前行为的函数,而不是 operator[])?也许这样可以提供更清晰的代码?我不知道。

另一个答案是它已经以这种方式存在,所以为什么不保留它,但可能当他们(stl 之前的那些)选择以这种方式实现它时,他们发现它提供了优势或什么?所以我的问题基本上是:为什么选择以这种方式实现它,这意味着与其他 operator[] 缺乏一致性。它有什么好处?

谢谢

【问题讨论】:

对于 std::vector 的 operator[] 未选中。如果您尝试访问向量范围之外的内容,它会愉快地返回对无效对象的引用。如果您需要检查,请使用 at() 方法。另一方面 std::map 不能这样做。 map 的简单解决方法就是使用 find() 方法。 std::vector operator[] 不能保证不被选中,只是不需要被选中。 @David Thornley:谢谢,我不知道。 我只是希望他们有一个const 版本的订阅运算符,我准备付出例外的代价来避免使用map::find + map::end,真的。 是的,这是 STL 最愚蠢的功能之一。我们大多数人认为 [] 是一个读取操作,但它也用于通过返回一个非常量引用(然后您可以分配给它)来进行写入。您不希望引用无效,因为人们经常会朝自己的脚开枪。解决这个问题的一种方法(正如 Klatchko 指出的那样)是让 operator[] 抛出异常。那将是我的首选设计,但 STL 似乎对异常过敏。这就是 Python 所做的(它甚至不需要担心无效的引用),而且它 Just Makes Sense(TM)。 【参考方案1】:

因为operator[] 返回对值本身的引用,所以指出问题的唯一方法是抛出异常(通常,STL 很少抛出异常)。

如果您不喜欢这种行为,可以改用map::find。它返回一个迭代器而不是值。这允许它在未找到该值时返回一个特殊的迭代器(它返回map::end),但还需要您取消对迭代器的引用以获取该值。

【讨论】:

但是vector也不返回引用吗?这里有什么区别?抱歉,我不熟悉 std::map 的内部,所以这可能从内部很明显。 使用 [] 越界访问向量是未定义的行为。我猜 map 在这种情况下也可以调用一些未定义的行为,但你真的喜欢吗?使用向量很容易找出索引是否有效,使用地图则不是(您可以先调用 find ,在这种情况下调用 [] 毫无意义,因为您应该已经找到了您的项目正在寻找)。 @n1ck: std::vector 将返回一个无效的引用(因为它很容易做)。 std::map 没有简单的方法来返回引用。 传统上,地图的接口(在所有语言中,不仅仅是C++)允许你写map[key] = value;之类的东西,地图将替换现有的值插入根据需要一个新的。这是一个长期存在的约定,因此std::map 遵循它也就不足为奇了。然而,由于operator[] 无法判断它必须返回的引用是否将用于operator= 的左侧,因此它必须始终假设最坏的情况,因此插入一个新元素。 @N1ck:不过,它们可能比 std 库更老。 :)【参考方案2】:

标准说 (23.3.1.2/1) operator[] 返回(*((insert(make_pair(x, T()))).first)).second。这就是原因。它返回引用T&。没有办法返回无效的引用。它返回参考,因为我猜它非常方便,不是吗?

【讨论】:

好的,方便operator[]返回引用。所以它应该插入一些东西,因为没有办法返回无效的引用。 他们为什么不制作一个抛出异常的 const 版本? 因为例外是针对特殊情况的。 @sbi: 除非你已经知道它的存在?但是,是的,我刚刚意识到,如果它像我想要的那样工作,它基本上是一个发现,谢谢。 与尼克斯的想法差不多。每次我使用地图进行查找时,我都不得不检查 find 是否返回 end,然后抛出异常,因为这是唯一合适的做法。【参考方案3】:

回答您真正的问题:没有令人信服的解释来说明为什么要这样做。 “只是因为”。

由于std::map 是一个关联 容器,因此在映射中没有明确的预定义键范围必须存在(或不存在)(与@987654323 的情况完全不同) @)。这意味着使用std::map,您需要非插入和插入查找功能。可以以非插入方式重载[] 并提供插入功能。或者可以反过来做:将[] 重载为插入运算符并提供非插入搜索功能。因此,有时有人决定采用后一种方法。这就是它的全部。

如果他们反过来做,也许今天有人会在这里问你问题的相反版本。

【讨论】:

我想这就是我想知道的,谢谢。第三种方法是不提供 operator[] 并在两个成员函数中提供查找和插入(没有运算符重载)。 @n1ck:[] 符号在人们的脑海中已经很好地固定下来了。理论上可以不使用它,但我认为这不太可能。 Pavel Minaev 上面给出的理由非常有说服力。 哪一个?这是一个“长期存在的约定”吗?可能是真的,但没有说服力。没有办法知道它是否在作业的左侧?这根本没有说服力,因为它可能只是抛出异常或声明未定义的行为。请记住,最初的问题是为什么它与 std::vector 不同。 查看一些令人信服的解释,例如在 this answer 下的 cmets 中。【参考方案4】:

这是为了分配目的:


void test()

   std::map<std::string, int >myMap;
   myMap["hello"] = 5;

【讨论】:

如果这是主要原因,那么常量映射将返回一个无法分配给的常量引用,并且常量映射的operator[] 不必创建值。但是常量映射甚至没有operator[],你一定是错的。 我不知道这让我怎么看错了......看起来如果常量映射上没有
operator[]
,它只是证明了我的观点。
常量映射没有operator[] 似乎表明operator[] 纯粹用于修改映射。我一定是误解了你的意思。如果 STL 的设计者不认为任何类型的 operator[] 对常量映射有意义,那么他们显然在修改映射时看到了 operator[] 的全部含义,就像答案中的赋值一样...... 拥有operator[] 有两种截然不同的语义,具体取决于地图是否为const 对我来说听起来是个坏主意。事实上,[] 具有某些明确定义的语义,const 地图根本不支持 - 因此它不存在。 @EToreo & sth:我可以看出我的观点有些薄弱。 &lt;sheepish_grin&gt; 让我...事实上,operator[] 有两个用途:您可以使用它来访问现有元素,并且可以插入元素。两者都有专门的功能,所以大概是它的组合,为什么存在operator[]。对于添加语义,您有insert(),对于访问语义,您有find() - 除了find() 可以返回“不存在的元素”,而运算符不能。 添加和查找需要插入语义才能使用operator[]【参考方案5】:

我认为这主要是因为在 map 的情况下(例如,与 vector 不同)它相当便宜且易于操作——您只需创建一个元素。在向量的情况下,他们可以扩展向量以使新的下标有效 - 但如果您的新下标远远超出现有的范围,那么将所有元素添加到该点可能会相当昂贵。当您扩展一个向量时,您通常还指定要添加的新元素的值(尽管通常使用默认值)。在这种情况下,将无法在现有元素和新元素之间的空间中指定元素的值。

地图的典型使用方式也存在根本差异。对于向量,通常在添加到向量中的事物与与向量中已有的事物一起工作的事物之间有一个清晰的界限。使用地图时,情况就不那么正确了——更多更常见的是看到代码会操纵存在的项目,或者如果它不存在则添加新项目。每个 operator[] 的设计都反映了这一点。

【讨论】:

嗯,我不确定当索引超出范围而没有用户干预时在向量中添加新元素是否正是我想要的行为。如果我访问一个越界数组,那可能是因为我在代码中的某个地方出错了。我不希望语​​言为我创建一个。对于第二部分,我给你 +1,尽管这并不是我个人对地图的体验。【参考方案6】:

它允许使用operator[] 插入新元素,如下所示:

std::map<std::string, int> m;
m["five"] = 5;

5 分配给m["five"] 返回的值,这是对新创建元素的引用。如果operator[] 不插入新元素,那将无法正常工作。

【讨论】:

查看我在***.com/questions/1639544/1639563#1639563 的评论,了解为什么我认为这是错误的。【参考方案7】:

map.insert(key, item);确保键在映射中但不会覆盖现有值。

map.operator[key] = item;确保键在映射中并用项目覆盖任何现有值。

这两个操作都很重要,足以保证一行代码。设计者可能选择了对 operator[] 更直观的操作,并为另一个创建了函数调用。

【讨论】:

【参考方案8】:

这里的区别在于 map 存储“索引”,即存储在 map 中(在其底层 RB 树中)的值是std::pair,而不仅仅是“索引”值。 总是有map::find() 会告诉您是否存在与给定密钥的配对。

【讨论】:

【参考方案9】:

答案是因为他们想要一个既方便又快速的实现。

向量的底层实现是一个数组。因此,如果数组中有 10 个条目并且您想要条目 5,则 T& vector::operator[](5) 函数只返回 headptr+5。如果您要求输入 5400,它会返回 headptr+5400。

地图的底层实现通常是一棵树。每个节点都是动态分配的,不像标准要求的向量是连续的。所以 nodeptr+5 没有任何意义,而 map["some string"] 并不意味着 rootptr+offset("some string")。

与使用地图查找一样,如果您想进行边界检查,矢量具有 getAt()。在向量的情况下,边界检查对于那些不想要它的人来说被认为是不必要的成本。在地图的情况下,不返回引用的唯一方法是抛出异常,对于那些不想要它的人来说,这也被认为是不必要的成本。

【讨论】:

难道他们不能返回一个无效的引用,就像你取消引用 std::map' end() 一样? @n1ck:没有。C++ 中没有无效的引用。 (这是它们与指针不同的主要方面之一。) n1ck,想想逻辑流程。要插入,程序必须找到将条目放入树中的内容。这种处理是有代价的。既然找不到值后最可能的操作是插入它,为什么不让 \[\] 自动插入。 如果你取消引用像 std::map's end 这样的东西怎么办?或者如果取消引用无效指针?可以实现相同的行为:如果它不存在:未定义的行为。 @jmucchiello:好的 cmets。我只是发现使用 operator[] 来达到这个目的并不直观,但你说得对。【参考方案10】:

考虑这样一个输入——3个block,每个block 2行,第一行是第二行的元素个数:

5
13 20 22 43 146
4
13 22 43 146
5
13 43 67 89 146

问题:计算所有三个块的第二行中存在的整数数。 (对于此示例输入,输出应为 3,因为所有三个块的第二行中都存在 13、43 和 146)

看看这段代码有多棒:

int main ()

    int n, curr;
    map<unsigned, unsigned char> myMap;
    for (int i = 0; i < 3; ++i)
    
        cin >> n;
        for (int j = 0; j < n; ++j)
        
            cin >> curr;
            myMap[curr]++;
        

    

    unsigned count = 0;
    for (auto it = myMap.begin(); it != myMap.end(); ++it)
    
        if (it->second == 3)
            ++count;
    

    cout << count <<endl;
    return 0;

根据标准operator[] 返回对(*((insert(make_pair(key, T()))).first)).second 的引用。这就是为什么我可以写:

myMap[curr]++;

它插入一个带有键curr 的元素,如果映射中不存在该键,则将该值初始化为零。尽管元素在地图中或不在地图中,它也会增加值。

看看有多简单?这很好,不是吗?这是一个很好的例子,它真的很方便。

【讨论】:

嗯,是的,这可能很方便也很好,但为什么要使用 operator[] 而不是更明确的命名函数呢?这种行为对我来说并不直观,这就是为什么我觉得奇怪的是像 operator[] 这样的行为,而不是使用具有明确名称的函数来执行一些不直接直观的奇怪处理。我并不是说拥有它不好,但为什么 operator[] 有这种行为,它可能是一个标准功能。 我猜你对其他编程语言有很好的经验,不是吗? :) 是也不是,你为什么要问?我主要是 C++ 人,但我确实知道其他几种语言。 我以为你不是 C++ 人,所以你问这个问题。但似乎还有另一个原因:) 我问这个问题的原因是因为行为不直观,我想知道为什么它是这样编码的。我当然知道它是如何起作用的,所以我对此没有任何问题。感谢您的回答,但这不是我想要的。【参考方案11】:

我知道这是一个老问题,但似乎没有人能很好地回答 IMO。到目前为止,我还没有看到任何提及这一点:

要避免未定义行为的可能性!如果除了 UB 之外还有任何合理的行为,那么我想我们应该这样做。

std::vector/array 表现出未定义的行为和错误的operator[] 索引,因为实际上没有合理的选择,因为这是您可以在 c/c++ 中做的最快、最基本的事情之一,尝试是错误的检查任何东西。检查是 at() 的用途。

std::*associative_container* 已经完成了查找索引元素将去哪里的工作,因此在那里创建一个并返回它是有意义的。这是非常有用的行为,operator[] 的替代品看起来不那么干净,但即使创建和插入新项目不是您想要的,或者对您没有用处,它仍然比未定义的行为好得多.

我认为 operator[] 是使用关联容器的首选语法,为了可读性,对我来说这非常直观,并且与数组的 operator[] 的概念完全匹配:返回对项目的引用在那个位置,使用或分配给

如果我对“如果那里什么都没有怎么办”的直觉只是“未定义的行为”,那么我绝对不会更糟,因为我会尽我所能避免这种情况,句号。

然后有一天我发现我可以插入带有operator[]的项目...生活变得更好了。

【讨论】:

【参考方案12】:

如果您想从 std::map 中读取带有某个键的元素, 但你不确定它是否存在, 如果没有,您不想意外插入它, 而是想要抛出异常, 但您也不想在每次读取元素时手动检查map.find(key) != map.end()

只需使用map::at(key) (C++11)

https://www.cplusplus.com/reference/map/map/at/

【讨论】:

【参考方案13】:

无法避免创建对象,因为 operator[] 不知道如何使用它。

myMap["apple"] = "green";

char const * cColor = myMyp["apple"];

我建议地图容器应该添加一个类似的功能

if( ! myMap.exist( "apple")) throw ...

它比阅读更简单更好

if( myMap.find( "apple") != myMap.end()) throw ...

【讨论】:

不确定您的意思是“因为 operator[] 不知道如何使用它”。如果您阅读我对其他答案的回答,就像我说的那样,我的第一个期望是像矢量一样行事并在无效索引上崩溃。即向量 v; v[1] = 1;会崩溃,为什么不用地图? @n1ckp 在向量的情况下,不允许分配给当前大小之外的索引。然而,在地图中,可以分配给当前不存在的索引。 @BlueSilver:这实际上是我的问题,为什么它允许用于地图(但不允许用于矢量)?我知道这些课程是如何运作的,我只是想知道为什么它是这样制作的。请阅读其他 cmets 和答案,因为我认为这已经讨论过了。

以上是关于如果键不存在,为啥 std::map operator[] 会创建一个对象?的主要内容,如果未能解决你的问题,请参考以下文章

gcc std::unordered_map 实现速度慢吗?如果是这样 - 为啥?

std::map的insert和下标[]操作区别

为啥我插入 std::map 失败?

为啥我不能增加 std::unordered_map 迭代器?

为啥我可以在 std::map<std::string, int> 中使用 const char* 作为键

为啥允许 std::unordered_map::rehash() 使迭代器无效?