Effective STL [22] | 避免原地修改set和multiset的键
所有标准关联容器,set和multiset保持它们的元素有序,这些容器的正确行为依赖于它们保持有序。
如果你改了关联容器里的一个元素的值(例如,把10变为1000),新值可能不在正确的位置,而且那将破坏容器的有序性。
修改map和multimap值
试图改变这些容器里的一个键值的程序将不能编译:
|
|
map<K, V>
或multimap<K, V>
类型的对象中元素的类型是pair<const K, V>
。因为键的类型const K,它不能改变。
|
|
如果你使用一个const_cast
,或许能改变它,后面会讨论到。
修改set和multiset值
对于set<T>
或multiset<T>
类型的对象来说,储存在容器里的元素类型只不过是T
,并非const T
。因此,set
或multiset
里的元素可能在你想要的任何时候改变。不需要映射。
|
|
为什么set或multiset里的元素不是常数?
假设我们有一个雇员的类:
|
|
让我们做合理的假设,每个雇员有唯一的ID号,就是idNumber
函数返回的数字。然后,建立一个雇员的set
,很显然应该只以ID
号来排序set
:
|
|
实际上,雇员的ID
号是set
中元素的键。其余的雇员数据只是虚有其表。在这里,没有理由不能把一个特定雇员的头衔改成某个有趣的东西:
|
|
因为在这里我们只是改变雇员的一个与set排序的方式无关的方面(一个雇员的非键部分),所以这段代码不会破坏set。那是它合法的原因。但它的合法排除了set/multiset
的元素是const
的可能。而且那是它们为什么不是的原因。
因为set
或multiset
里的值不是const
,所以试图改变它们可以编译。
如果你改变set
或multiset
里的元素, 你必须确保不改变一个键部分——影响容器有序性的元素部分。如果你做了,你会破坏容器,再使用那个容器将产生未定义的结果, 而且那是你的错误。另一方面,这个限制只应用于被包含对象的键部分。对被包含元素的所有其他部分来说,是开放的: 随便改变!
阻止修改set和multiset值
即使set
和multiset
的元素不是const
,实现仍然有很多方式可以阻止它们被修改。
例如,实现可以让用于set<T>::iterator的operator*
返回一个常数T&
。即,它可以让set
的迭代器解引用的结果是set
元素的常量引用。
在这样的实现下,将没有办法修改set
或multiset
的元素,因为所有访问那些元素的方法都将在让你访问之前加一个const
。
要不要修改set和multiset值
- 如果不关心移植性,你想要改变set或multiset中元素的值,而且你的STL实现让你侥幸成功,继续做。只是要确定不要改变元素的键部分,即,会影响容器有序性的元素部分。
- 如果在乎移植性,就认为set和multiset中的元素不能被修改,至少不能在没有映射的情况下
Solution: 映射到一个引用
有时候完全有理由改变set
或multiset
元素的非键部分。例如刚看的不能在一些实现下编译的setTitle
调用:
|
|
为了让它可以编译并且行为正确,我们必须映射掉*i的常量性。这是那么做的正确方法:
|
|
这可以得到i指向的对象,告诉你的编译器把映射的结果当作一个(非常数)Employee的引用,然后在那个引用上调用setTitle。
错误版本的映射
|
|
它也等价于如下内容:
|
|
这两个都能编译,而且因为它们等价,所以它们错的原因也相同。
在运行期,它们不能修改*i
!
在这两个情况里,映射的结果是一个*i
副本的临时匿名对象,而setTitle是在匿名的物体上调用,不在*i
上!*i
没被修改,因为setTitle从未在那个对象上调用,它在那个对象的副本上调用。
两个句法形式等价于这个:
|
|
通过映射到引用,我们避免了建立一个新对象。
取而代之的是,映射的结果是一个现有对象的引用,i
指向的对象。
当我们在有这个引用指定的对象上调用setTitle时,我们是在*i
上调用setTitle,而且那正是我们想要的。
为什么不能去掉map和multimap的常量性
注意map<K, V>
或multimap<K, V>
包含pair<const K, V>
类型的元素。
那个const
表明pair
的第一个组件被定义为常量,而那意味着试图修改它是未定义的行为(即使映射掉它的常量性)。
理论上,一个STL实现可能把这样的值写到一个只读的内存位置(比如,一旦写了就通过系统调用进行写保护的虚拟内存页),而且试图映射掉它的常量性,最多,没有效果。
如果你是一个坚持遵循标准拟定的规则的人,你绝不会试图映射掉map
或multimap
键的常量性。
安全修改元素步骤
想要总是可以工作而且总是安全地改变set、multiset、map或multimap里的元素,按5个简单的步骤去做:
- 定位你想要改变的容器元素。如果你不确定最好的方法, 条款45提供了关于怎样进行适当搜寻的指导。
- 拷贝一份要被修改的元素。对map或multimap而言,确定不要把副本的第一个元素声明为const。毕竟,你想要改变它!
- 修改副本,使它有你想要在容器里的值。
- 从容器里删除元素,通常通过调用erase(参见条款9)。
- 把新值插入容器。如果新元素在容器的排序顺序中的位置正好相同或相邻于删除的元素,使用insert的“提示”形式把插入的效率从对数时间改进到分摊的常数时间。使用你从第一步获得的迭代器作为提示。
一句话概括就是:先删除,再插入新的。
Example:
这是同一个累人的雇员例子,这次以安全、可移植的方式写:
|
|