相同的意义可能是相等,也可能是等价,该条款就是讨论相等和等价的区别。【要注意,许多函数都必须判断两个值是否相同】
相等的概念是基于 operator==
的。【find()
函数对相同的定义是相等】
x
和 y
,如果表达式 x == y
返回 true
,即 x
和 y
有相等的值;但重要的是 operator==
是可以重载的,它们自身的成员未必一一相等。等价关系是以在已排序的区间中对象值的相对顺序为基础的。【set
的 insert()
成员函数对相同的定义是等价】
这句话的意思是:对于两个对象 x
和 y
,如果按照关联容器 c
的排列顺序,每个都不在另一个的前面,那么称这两个对象按照 c
的排列顺序有等价的值。
例如 set
的默认比较函数是 less
,在默认情况下 less
只是简单调用了 T
的 operator <
;所以对于两个 T
对象 t1
和 t2
来说,只要下面表达式结果为真,就说 t1
和 t2
对于 operator <
有等价的值:
!(w1 < w2) && !(w2 < w1)
// 更一般的情况是用户自定义的判别式
// 每个关联容器都通过key_comp成员函数使排序判别式可被外界使用
!c.key_comp()(x, y) && !c.key_comp()(y, x)
进一步理解相等和等价的区别,考虑一个不区分大小写的 set
,代码如下:
struct CIStringCompare : public binary_function<string, string, bool>{
bool operator()(const string& lhs, const string& rhs) const{
return ciStringCompare(lhs, rhs);//不区分大小写的函数对象,具体实现参考条款35
}
}
set<string, CIStringCompare> ciss;// 不区分大小写的set
// 【下面的分析也说明要多使用容器的成员函数(list::find)而不是非成员函数(find)】
// 如果在set中插入STL和stl,
// 只有第一个字符串会被插入,因为第二个和第一个等价;
// 如果使用set的find成员函数查找stl,
// 是可以查找成功的(也是因为stl和STL等价);
// 如果使用非成员的find算法就会查找失败。
标准关联容器是基于等价而不是相等,但是为什么关联容器要使用等价,而不是相等呢?
最后总结一下:
每当程序员要创建包含指针的关联容器时,一定要记住,容器将会按照指针的值进行排序。
绝大多数情况下,这不会是程序员所希望的,所以肯定要创建自己的函数子类作为该容器的比较类型(comparison type),代码如下:
struct StringPtrLess : public std::binary_function<const std::string*, const std::string*, bool> { // std::binary_function在C++11中已被废弃
bool operator()(const std::string* ps1, const std::string* ps2) const
{
return *ps1 < *ps2;
}
};
struct DereferenceLess {
template<typename PtrType>
bool operator()(PtrType pT1, PtrType pT2) const
{
return *pT1 < *pT2;
}
};
int test_item_20()
{
//std::set ssp;
// 相当于std::set>, 得不到预期的结果
//std::set ssp;
std::set<std::string*, DereferenceLess> ssp; // 与std::set ssp;的行为相同
ssp.insert(new std::string("Anteater"));
ssp.insert(new std::string("Wombat"));
ssp.insert(new std::string("Lemur"));
ssp.insert(new std::string("Penguin"));
for (auto it = ssp.cbegin(); it != ssp.cend(); ++it) {
fprintf(stdout, "%s\n", (**it).c_str());
}
return 0;
}
如果有一个包含智能指针或迭代器的容器,那么也要考虑为它指定一个比较类型,对指针的解决方案同样也适用于那些类似指针的对象。
DereferenceLess
适合作为包含 T*
的关联容器的比较类型一样,对于容器中包含了指向 T
对象的迭代器或智能指针的情形,DereferenceLess
也同样可用作比较类型。比较函数的返回值表明的是按照该函数定义的排列顺序,即一个值是否在另一个之前。
相等的值从来不会有前后顺序关系,所以,对于相等的值比较函数应当始终返回 false
。
考虑错误的情况,如果对于一个 set
,不用 less
作为比较类型,而用 less_equal
(即 ≤
),就相当于在等值情况下返回 true
,那么最后影响会如下所示:
set<int, less_equal<int> > s; // s以“<=”排序
s.insert(10); // 插入10A
s.insert(10); // 插入10B
// set遍历它的内部数据结构以查找哪儿适合插入10B。
// 最终,它总要检查10B是否与10A相同。(即判断是否等价)
!(10_A <= 10_B) && !(10_B <= 10_A) // 测试10A和10B是否等价
// 上述表达式简化为:
!(true) && !(true)
// 结果自然就是false
// 最后set就判定10A和10B不等价,又一次将10插入了set
前面一个结论还是比较直观的,但还是要清楚违反这个规则造成的后果可能并不那么直白。
首先利用条款 20 中的代码进行修改,代码的原意是想将其排序方式变成降序:
struct StringPtrGreater: // 高亮显示
public binary_function<const string*, // 这段代码和书中89页的改变
const string*, // 当心,这代码是有瑕疵的!
bool> {
bool operator()(const string *ps1, const string *ps2) const
{
return !(*ps1 < *ps2); // 只是取反了旧的测试;
} // 这是不对的!
};
<
不会得到 >
,它给你的是 >=
。true
,对关联容器来说,它是一个无效的比较函数。最后,讨论最让人迷惑的一种情况,即 multi 系列容器如果没有遵守本条规则究竟会产生什么后果:
// 假设当前有一 multiset:
multiset<int, less_equal<int> > s; // s仍然以“<=”排序
s.insert(10); // 插入10A
s.insert(10); // 插入10B
// 问题在于后续的操作:
equal_range(s.begin(), s.end(), 10);// 返回等于10的迭代器范围
// 前面说过了,关联容器中的比较都是基于等价的
// 所以这里的equal_range是取等价为10的值的范围
// 检查10A和10B等价与否,用下面的表达式:
!(10_A <= 10_B) && !(10_B <= 10_A)
// 会变成如下结果:
!(true) && !(true)
// 最后结果当然就是false
// 所以最后结果表明10A不等价于10B,
// equal_range就会失效
本条款讨论的内容很直观。
set
和 multiset
保持它们的元素有序,这些容器的正确行为依赖于它们保持有序;如果改变了某个值(例如把 10
变为 1000
),新值可能不在正确的位置上,而且那将破坏容器的有序性。map
或 multimap
类型的对象而言,元素的类型是 pair
;所以键是不允许改变的。【但本条款重心不在 map
和 multimap
键值】对于 set
或 multiset
类型的对象,容器中元素的类型是 T
,而不是 const T
;虽然没有 const
特性,但不能轻易地直接修改键值。
假设一个场景如下:
// 有一个雇员类,即set用来存储的类
class Employee {
public:
...
const string& name() const; // 获取雇员名
void setName(const string& name); // 设置雇员名
const string& getTitle() const; // 获取雇员头衔
void setTitle(string& title); // 设置雇员头衔
int idNumber() const; // 获取雇员ID号
...
}
// 该函数类是用作比较函数,即只取雇员类的idNumber来排序
struct IDNumberLess{
public binary_function<Employee, Employee, bool> { // 参见条款40
bool operator()(const Employees lhs,
const Employee& rhs) const
{
return lhs.idNumber() < rhs.idNumber();
}
};
typedef set<Employee, IDNumberLess> EmpIDSet;
EmpIDSet se; // se是雇员的set,
// 按照ID号排序
修改键值的操作如下:
// 修改操作如下所示:
Employee selectedID; // 容纳被选择的雇员
... // ID号的变量
EmpIDSet::iterator i = se.find(selectedID);
if (i != se.end()){
i->setTitle("Corporate Deity"); // 给雇员新头衔
}
// 有些STL实现会拒绝这行
// 因为*i是const
前面例子旨在说明:
set
和 multiset
的元素不是 const
,实现仍然有很多方式可以阻止它们被修改。set::iterator
的 operator*
返回一个常数 T&
。(即前面修改键值的操作结果就是如此,假设 *i
是 const
的,所以 STL 实现就会拒绝设置新头衔的那一行)想要解决问题,其实将其常量移除即可,代码如下:【要注意必须强制转换到引用】
// 正确的版本:【映射到引用】
if (i != se.end()) {// 转换掉*i的常量性
const_cast<Employee&>(*i).setTitle("Corporate Deity");
}
// 错误的版本:【可通过编译,但是实际是错误的】
// 版本一:
if (i != se.end()){ // 把*i转换成一个Employee
static_cast<Employee>(*i).setTitle("Corporate Deity");
}
// 等价的版本二:
if (i != se.end()) {// 同上,但使用C方式的类型转换
((Employee)(*i)).setTitle("Corporate Deity");
}
// 等价的版本三:(看清楚究竟不转成引用为什么会出错)
if (i != se.end()){
Employee tempCopy(*i); // 把*i拷贝到tempCopy
tempCopy.setTitle("Corporate Deity"); // 修改tempCopy,*i根本没有修改
}
前面已经将本条款的内容讲解了大半,总结如下:
set
和 multiset
虽然键值本身不是 const
的,但是有很多其他操作导致它的修改会被阻止;所以用强制转换可以移除 const
来进行修改。【要注意它本身不是 const
,只不过经过 set
或 multiset
的函数才转成了 const
】map
和 multimap
的键值本身是 const
的,所以最好就不要强制转换移走常量特性。大多数的强制转换是可以避免的,在本条款的最后,作者提出了按照五个步骤去安全改变 set
、multiset
、map
或 multimap
里的元素,步骤如代码所示:
EmpIDSet se; // 同前,se是一个以ID号排序的雇员set
Employee selectedID; // 同前,selectedID是一个带有需要ID号的雇员
...
EmpIDSet::iterator i =
se.find(selectedID); // 第一步:找到要改变的元素
if (i!=se.end()){
Employee e(*i); // 第二步:拷贝这个元素
se.erase(i++); // 第三步:删除这个元素;
// 自增这个迭代器以保持它有效(参见条款9)
e.setTitle("Corporate Deity"); // 第四步:修改这个副本
se.insert(i, e); // 第五步:插入新值;提示它的位置
// 和原先元素的一样
}
set
、multiset
、map
和 multimap
的确定的对数时间查找能力。
vector
可能效率也更高)vector
可以比标准关联容器更快。
vector
的前提下才有可能比一个关联容器能提供更高的性能;因为只有有序容器才能正确地使用查找算法:binary_search()
、lower_bound()
、equal_range()
等。next
节点和 prev
节点);假设数据结构比较大,需要分裂成多个页面,vector
就比关联容器需要更少的页面(即更少的页面错误)。
vector
那种内存组织方式会使得页面错误最少。vector
的缺点是它必须保持有序,同时它的插入和删除代价也比关联容器更昂贵。
vector
代替关联容器才有意义。vector
模拟 set
,代码骨架如下:vector<Widget> vw; // 代替set
... // 建立阶段:很多插入,
// 几乎没有查找
sort(vw.begin(), vw.end()); // 结束建立阶段。(当
// 模拟一个multiset时,你
// 可能更喜欢用stable_sort
// 来代替;参见条款31。)
Widget w; // 用于查找的值的对象
... // 开始查找阶段
if (binary_search(vw.begin(), vw.end(), w))... // 通过binary_search查找
vector<Widget>::iterator i =
lower_bound(vw.begin(), vw.end(), w); // 通过lower_bound查找
if (i != vw.end() && !(w < *i))... // 条款19解释了
// “!(w < *i)”测试
pair<vector<Widget>::iterator,
vector<Widget>::iterator> range =
equal_range(vw.begin(), vw.end(), w); // 通过equal_range查找
if (range.first != range.second)...
... // 结束查找阶段,开始
// 重组阶段
sort(vw.begin(), vw.end()); // 开始新的查找阶段...
vector
代替 map
或者 multimap
时,有些有趣的结论。
map
中存储对象类型是 pair
,但是 vector
模拟 map
时需要去掉 const
(对 vector
进行排序的时候需要对元素赋值);所以 vector
模拟 map
时存储对象类型是 pair
。
当排序 vector
时也必须为 pair
写一个自定义比较函数(这个比较函数是用作比较 value
,这就是模拟 map
的行为)。
同时还需要另外一个比较函数来查找(只作用在 key
上)。
将上述结论用代码描述:
// 用有序vector模拟map
typedef pair<string, int> Data; // 在这个例子里
// "map"容纳的类型
class DataCompare { // 用于比较的类
public:
bool operator()(const Data& lhs, // 用于排序的比较函数
const Data& rhs) const
{
return keyLess(lhs.first, rhs.first); // keyLess在下面
}
bool operator()(const Data& Ihs, // 用于查找的比较函数
const Data::first_type& k) const // (形式1)
{
return keyLess(lhs.first, k);
}
bool operator()(const Data::first_type& k, // 用于查找的比较函数
const Data& rhs) const // (形式2)
{
return keyLess(k, rhs.first);
}
private:
bool keyLess(const Data::first_type& k1, // “真的”
const Data::first_type& k2) const // 比较函数
{
return k1 < k2;
}
};
有序 vector
模拟 map
的代码骨架和前面模拟 set
的差不多,只是必须把 DataCompare
对象用作比较函数。
vector<Data> vd; // 代替map
... // 建立阶段:很多插入,
// 几乎没有查找
sort(vd.begin(), vd.end(), DataCompare()); // 结束建立阶段。(当
// 模拟multimap时,你
// 可能更喜欢用stable_sort
// 来代替;参见条款31。)
string s; // 用于查找的值的对象
... // 开始查找阶段
if (binary_search(vd.begin(), vd.end(), s,
DataCompare()))... // 通过binary_search查找
vector<Data>::iterator i =
lower_bound(vd.begin(), vd.end(), s,
DataCompare()); // 在次通过lower_bound查找,
if (i != vd.end() && !DataCompare()(s, *i))... // 条款45解释了
// “!DataCompare()(s, *i)”测试
pair<vector<Data>::iterator,
vector<Data>::iterator> range =
equal_range(vd.begin(), vd.end(), s,
DataCompare()); // 通过equal_range查找
if (range.first != range.second)...
... // 结束查找阶段,开始
// 重组阶段
sort(vd.begin(), vd.end(), DataCompare()); // 开始新的查找阶段...
map
的 operator[]
函数与众不同,它与 vector
、deque
和 string
的 operator[]
函数无关,与用于数组的内置 operator[]
也没有关系。
相反,map::operator[]
的设计目的是为了提供添加和更新(add or update)的功能,map::operator[]
返回一个引用。
从语句 m[k] = v
出发讨论 operator[]
工作的原理:
k
关联的值对象的引用,然后 v
赋值给所引用(从 operator[]
返回的)的对象。operator[]
可以用来返回引用的值对象;operator[]
就没有可以引用的值对象,此时它使用值类型的默认构造函数从头开始建立一个,然后 operator[]
返回这个新建立对象的引用。代码分析如下:
map<int, Widget> m;
// 第一种情况:用operator[]直接插入
m[1] = 1.50;// 当前m中没有该映射,是直接插入
// 上面这个语句相当于调用operator[]:
m.operator[](1) = 1.50;
// 功能上等价于以下语句:
typedef map<int, Widget> IntWidgetMap; // 方便的typedef
pair<IntWidgetMap::iterator, bool> result = m.insert(IntWidgetMap::value_type(1, Widget()));
// 用键1建立新映射入口和一个默认构造的值对象;
result.first->second = 1.50;
// 第二种情况:用insert直接插入
m.insert(IntWidgetMap::value_type(2, 1.50));
// 与前面的情况相比,节省了三个函数调用:
//Ⅰ-默认构造函数带来的临时对象;
//Ⅱ-析构该临时对象;
//Ⅲ-调用赋值操作符。
// 第三种情况:用insert来更新
m[1] = 2.0;
m.insert(IntWidgetMap::value_type(1, 2.0)).first->second = 2;
// 在更新的时候使用insert,也同样要承受额外的构造和析构函数
对效率的考虑使我们得出结论:
insert
,而不是 operator[]
;operator[]
。最后其实可以写一个函数去判断当前情况究竟是更新还是插入,再根据结果考虑用 operator[]
还是 insert
:
template<typename MapType, // map的类型
typename KeyArgType, // KeyArgType和ValueArgtype是类型参数
typename ValueArgtype>
typename MapType::iterator
efficientAddOrUpdate(MapType& m,
const KeyArgType& k,
const ValueArgtype& v)
{
typename MapType::iterator Ib = m.lower_bound(k);// 找到k在或应该在哪里,注意这里用了typename
if(Ib != m.end() && // 如果Ib指向一个pair
!(m.key_comp()(k, Ib->first))) { // 它的键等价于k...
Ib->second = v; // 更新这个pair的值
return Ib; // 并返回指向pair的迭代器
}
else{
typedef typename MapType::value_type MVT;
return m.insert(Ib, MVT(k, v)); // 把pair(k, v)添加到m并返回指向新map元素的迭代器
} // 注意这里是insert的hint版本,后续用MVT(k, v)表明了插入位置
}
unordered_set
、unordered_multiset
、unordered_map
和 unordered_multimap
等。
equal_to()
作为默认的比较函数的(这与关联容器默认使用的 less()
不同)。