一、问题引入
正如所有标准关联容器,set和multiset保持它们的元素有序,这些容器的正确行为 依赖于它们保持有序。 如果你改了关联容器里的一个元素的值(例如,把10变为1000),新值可能不在正确的位置,而且那将破坏容器的有序性。很简单,是吗?
这对于map和multimap特别简单,因为试图改变这些容器里的一个键值的程序将不能编译: map<int, string> m; ...
// 错误!map键不能改变 m.begin()->first = 10; multimap<int, string> mm; ...
// 错误!multimap键也不能改变 mm.begin()->first = 20;
那是因为map<K, V>或multimap<K, V>类型的对象中元素的类型是pair<const K, V>。因为键的类型const K,它不能改变。(嗯,如果你使用一个const_cast,正如我们将在下面看到的,你或许能改变它。不管相信与否,有时那是你想要做的。)
但注意本条款的标题没有提及map或multimap。那有一个原因。正如上面例子演示的,原地修改键对map和multimap来说是不可能的(除非你使用映射),但是它对set和multiset却是可能的。对于set<T>或multiset<T>类型的对象来说,储存在容器里的元素类型只不过是T,并非const T。因此,set或multiset里的元素可能在你想要的任何时候改变。不需要映射。(实际上,事情不完全那么简单,但我们不久将看到。没理由超过自己。)
二、解决思路
让我们从明白为什么set或multiset里的元素不是常数开始。假设我们有一个雇员的类: class Employee { public: ...
// 获取雇员名 const string& name() const;
// 设置雇员名 void setName(const string& name);
// 获取雇员头衔 const string& getTitle() const;
// 设置雇员头衔 void setTitle(string& title);
// 获取雇员ID号 int idNumber() const; ... }
如你所见,我们可以得到雇员各种各样的信息。不过,让我们做合理的假设,每个雇员有唯一的ID号,就是idNumber函数返回的数字。 然后,建立一个雇员的set,很显然应该只以ID号来排序set:
struct IDNumberLess : public binary_function<Employee, Employee, bool> { bool operator()(const Employees lhs, const Employee& rhs) const{ return lhs.idNumber() < rhs.idNumber(); } };
// se是雇员的set,按照ID号排序 typedef set<Employee, IDNumberLess> EmpIDSet; EmpIDSet se;
实际上,雇员的ID号是set中元素的键。其余的雇员数据只是虚有其表。在这里,没有理由不能把一个特定雇员的头衔改成某个有趣的东西。像这样:
// 容纳被选择的雇员ID号的变量 Employee selectedID; ... EmpIDSet::iterator i = se.find(selectedID); if (i != se.end()){
// 给雇员新头衔
// 有些STL实现会拒绝这行,因为*i是const i->setTitle("Corporate Deity"); }
因为在这里我们只是改变雇员的一个与set排序的方式无关的方面(一个雇员的非键部分),所以这段代码不会破坏set。那是它合法的原因。但它的合法排除了set/multiset的元素是const的可能。而且那是它们为什么不是的原因。
正如上面说的:有些STL实现会拒绝这行,我们如何做呢?
● 如果不关心移植性,你想要改变set或multiset中元素的值,而且你的STL实现让你侥幸成功,继续做。只是要确定不要改变元素的键部分,即,会影响容器有序性的元素部分。 ● 如果你在乎移植性,就认为set和multiset中的元素不能被修改,至少不能在没有映射的情况下。
我们应该利用映射怎样做才能既正确又可移植?它不难,但是它用到了太多程序员忽略的一个细节:你必须映射到一个引用
比如说:
为了让它可以编译并且行为正确,我们必须映射掉*i的常量性。这是那么做的正确方法: if (i != se.end()) { const_cast<Employee&>(*i).setTitle("Corporate Deity"); }
很多人想到的是这段代码:
// 把*i映射到一个Employee if (i != se.end()){ static_cast<Employee>(*i).setTitle("Corporate Deity"); } 它也等价于如下内容: if (i != se.end()) { ((Employee)(*i)).setTitle("Corporate Deity"); }
两个句法形式等价于这个:
// 把*i拷贝到tempCopy,修改tempCopy if (i != se.end()){ Employee tempCopy(*i); tempCopy.setTitle("Corporate Deity"); }
在运行期,它们不能修改*i!在这两个情况里,映射的结果是一个*i副本的临时匿名对象,而setTitle是在匿名的物体上调用,不在*i上!*i没被修改,因为setTitle从未在那个对象上调用,它在那个对象的副本上调用
三、问题拓展:map和multimap如何改变值
前面写的适用于set和multiset,但当我们转向map和multimap时,细节变粗了。注意map<K, V>或multimap<K, V>包含pair<const K, V>类型的元素
大多数映射可以避免,那包括我们刚刚考虑的。如果你要总是可以工作而且总是安全地改变set、multiset、map或multimap里的元素,按五个简单的步骤去做: 1. 定位你想要改变的容器元素。 2. 拷贝一份要被修改的元素。对map或multimap而言,确定不要把副本的第一个元素声明为const。毕竟,你想要改变它! 3. 修改副本,使它有你想要在容器里的值。 4. 从容器里删除元素,通常通过调用erase 5. 把新值插入容器。如果新元素在容器的排序顺序中的位置正好相同或相邻于删除的元素,使用insert的“提示”形式把插入的效率从对数时间改进到分摊的常数时间。使用你从第一步获得的迭代器作为提示。
这是同一雇员例子,这次以安全、可移植的方式写: EmpIDSet se; Employee selectedID; ...
// 第一步:找到要改变的元素 EmpIDSet::iterator i =se.find(selectedID); if (i!=se.end()){
// 第二步:拷贝这个元素 Employee e(*i);
// 第三步:删除这个元素;自增这个迭代器以保持它有效 se.erase(i++); // 第四步:修改这个副本 e.setTitle("Corporate Deity");
// 第五步:插入新值;提示它的位置和原先元素的一样 se.insert(i, e); } 你将原谅我以这种方法放它,但要记得关键的事情是对于set和multiset,如果你进行任何容器元素的原地修改,你有责任确保容器保持有序
|