为什么 std::map::operator[] 如此违反直觉?
在我看来,通常用于访问数据结构的运算符突然被定义为将数据插入到数据结构中,这似乎是“邪恶的”(在 C++ FAQ 中)。
我想问题是“什么会更好”?对于某些类型的映射值,这个问题很容易回答;例如,如果我们将键映射到指针,您可能真的喜欢operator[]为不存在的键返回nullptr,但这显然不适用于其他类型。
它可能会在不存在的键上抛出异常,甚至默认构造一个临时值并返回它而不将其添加到映射中。对于这种容器类型,将 [] 从读取语义转变为写入语义的充分理由是什么?
It seems to me 'evil' (in the C++ FAQ sense of the word), for an operator which is generally used to access a data structure to suddenly be defined to insert data into a data structure.
I guess the issue is 'what would be better'? This question is answered easily for certain types of mapped value; for example, if we map keys to pointers, you might really like operator[] to return nullptr for a non-existent key, but that clearly doesn't work for other types.
It could throw an exception on non-existent key, or even default construct a temporary and return that without adding it to the map. What is the good reason for turning [] from read semantics to write semantics for this container type?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
基本问题是没有语法方法可以可靠地区分:
from
运算符的定义。因为它可能出现在任一位置,所以该类确保它可以处理这两个位置,并在必要时提供默认覆盖。如果您发现这是不合理的,那么您需要完全避免
std::map::operator[]
。另一个原因是当键不在列表中时必须有一些定义的行为。由于operator[]必须返回一个值(LValue或RValue),因此它不能返回空指针、尾后迭代器或任何其他标记值。唯一剩下的选择是提出例外。 STL 不会引发太多异常,因为即使在没有异常的情况下也可以使用它。必须选择其他一些行为,这就是结果。
解决此问题的最佳方法是使用不具有此行为的
std::map
成员函数。这将是map::find()
,如果未找到该键,它会返回map::end
。The basic problem is that there is no syntactic way to reliably distinguish:
from
in the operator's definition. Because it may appear in either location, the class makes sure that it can handle both, providing a default to overwrite, if necessary. If you find this to be unconscionable, then you need to avoid
std::map::operator[]
altogether.Another reason for this is there must be some defined behavior for when the key is not in the list. Since
operator[]
must return a value (either LValue or RValue), then it cannot return a null pointer, past-the-end iterator, or any other sentinel value. The only remaining option would be to raise an exception. The STL doesn't raise very many exceptions, because it is intended to be used even in cases where exceptions are not. Some other behavior must be chosen, and this is the result.The best way around this is to use a member function of
std::map
that doesn't have this behavior. That would bemap::find()
, which returnsmap::end
if the key is not found.“对于这种容器类型,将 [] 从读取语义转向写入语义的充分理由是什么?”
想了一会儿,我想到了两个原因。第一个原因是效率。它有助于反思实际的算法以及语义是否使生活变得更容易或更困难。当前语义中最突出的一种算法是累积与键相关的值。
在这种情况下,映射语义很好,因为您可以依赖将 count 中的每个值初始化为零,这可能就是您想要的。在这种情况下,我们只对每个键和值对在映射中进行一次搜索。
如果你将其与 Python 进行比较(如果按照你的建议,如果键不存在,它会抛出异常),你会得到更混乱且效率更低的代码,如下所示:
或者使用 get() 和默认值的稍微简洁的版本。
请注意,在这两个版本中,都会对每个键和值对执行两次字典搜索。根据您的要求,这可能是严厉的处罚。因此,在这种情况下,C++ 映射语义对于高效代码是必要的。
C++ 有 const,它是保护事物不被改变的绝佳工具。我有时怀疑 const 被严重低估了。在您的情况下,使用 const 将防止您使用运算符 [] 更改地图的内容。
这种行为的第二个充分理由是它与许多语言中关联数组的行为相同。几十年来,像 Awk 和 Perl 这样的语言对于关联数组都具有相同的行为。如果您来自这些语言,std::map 的行为可能非常直观。
"What is the good reason for turning [] from read semantics to write semantics for this container type?"
Having thought about it for a bit longer I can think of two reasons. The first reason is efficiency. It helps to reflect on actual algorithms and whether the semantics make life easier or harder. One algorithms where the current semantics shine is in accumulating values associated with keys.
The map semantics are nice in this case because you can rely on each value in count being initialised with zero which is likely to be what you want. In this case we only do one search into the map for each key and value pair.
If you compare that with Python (which throws an exception if the key is absent as you suggest), you get messier and less efficient code that looks like:
Or a slightly neater version using get() and default values.
Note that in both these version two searches into the dictionary are performed for each key and value pair. Which can be a severe penalty depending on your requirements. So the C++ map semantics are necessary for efficient code in this case.
C++ has const which is a wonderful facility for protecting things from changing. I sometimes suspect that const is massively under-appreciated. In your case using const will protect you from changing the contents of your map using operator[].
The second good reason for this behaviour is that it is the same as the behaviour of associative arrays in a number of languages. Languages like Awk and Perl have had the same behaviour for associative arrays for decades. If you are coming from these languages, the behaviour of std::map is probably very intuitive.
正如 TokenMacGuy 提到的, ::operator[] 的用法可能不明确,您所描述的是他们处理不明确性的方式。
作为软件开发人员需要注意的一件重要事情是,第 3 方库几乎从来都不是按照您使用它们的方式编写的。更糟糕的是,设计不当的第三方库可能会损害代码的质量。
您刚才描述的所有内容都可以通过抽象化 std::map 类来轻松完成,如果 ::operator[] 的副作用让您感到那么困扰,我会鼓励您将其抽象化。
As TokenMacGuy mentioned, the usage of the ::operator[] can be ambiguous, and what you described was their way of handling the ambiguity.
An important thing to look at as a software developer is that 3rd party libraries are almost never written exactly the way you would use them. Even worse, poorly designed 3rd party libraries can damage the quality of your code.
Everything you just described can be easily accomplished by abstracting the std::map class away, and if the side effects of ::operator[] bother you that much, I would encourage that you abstract it away.
要了解有关 STL 的大多数设计决策,需要了解其演变,尤其是 SGI STL< /a>.
map
是 Pair 关联容器 的模型以及唯一排序关联容器。第二个概念很重要,因为它与
set
共享,而通过 key 进行访问(使用operator[]
)是没有意义的。insert
的语义(不替换值)被设计为适合map
和set
(使得分解它们的实现成为可能) 。请注意,对于元素不可变的set
来说,替换也没有意义。因此,我相信,在引入
operator[]
时,选择了这种奇怪的语义,以便您可以为map
提供insert
的替代方案和multimap
,而不是稍微复杂一点的:老实说,我希望它的行为更像
find
(带有std::key_error
)。这种语义当然不直观。To understand most design decisions about the STL, one needs to look at its evolution, and notably at the SGI STL.
A
map
is a model of an Pair Associative Container and of a Unique Sorted Associative Container.The second concept is important because it is shared with
set
, for which an access by key (usingoperator[]
) just does not makes sense.The semantics of
insert
(which does not replace the value) were designed to fit bothmap
andset
(making it possible to factorize their implementation). Note that for aset
, whose elements are immutable, replacement does not makes sense either.It is my belief therefore, than when introducing
operator[]
, this weird semantic was chosen so that you would have an alternative toinsert
formap
andmultimap
, instead of the slightly more complicated:Honestly, I would have a preferred it to behave more like
find
(with astd::key_error
). This semantic certainly isn't intuitive.