关联容器
本文介绍在关联容器中常见的一些的问题以及提升使用关联容器的建议。
1. 理解相等(equality)和等价(equivalence)的区别。
相等是以operator==为基础的。等价是以operator<为基础的。
例如find的定义是相等,他用operator==来判断,这是比较容易理解的。
而等价关系是以“在已排序的区间中对象值的相对顺序”为基础的。也就是说,如果两个值中任何一个(按照既定的排列顺序)都在另一个的前面,那么他们就是等价的。
!(w1 < w2) && !(w2 < w1);
!c.key_comp()(x, y) && !c.key.comp(y, x);
key_comp()
返回一个函数,并以x, y作为参数传入。
具体而言,我们设想一个不区分string大小的set,来具体说明问题。
#include <iostream>
#include <set>
#include <string>
using namespace std;
bool ciCharLess(char c1, char c2) {
// 在C++中,char可能是有符号的也可能是无符号的,所以我们要转换为无符号的char,才能比较。
return tolower(static_cast<unsigned char> (c1)) <
tolower(static_cast<unsigned char> (c2));
}
bool ciStringCompare(const string& s1, const string& s2) {
return lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), ciCharLess);
}
// It must be a struct but not be a class.
struct CIstring : public binary_function<string, string, bool> {
bool operator() (const string& T1, const string& T2) const {
return ciStringCompare(T1, T2);
}
};
int main(int argc, char *argv[]) {
set<string, CIstring> ciss;
// ciss.insert("yan");
ciss.insert("Yan");
ciss.insert("yan");
copy(ciss.begin(), ciss.end(), ostream_iterator<string>(cout, "\n"));
if (ciss.find("yan") != ciss.end()) {
// base on equivalence.
cout << "find" << endl;
}
if (find(ciss.begin(), ciss.end(), "yan") != ciss.end()) {
// base on equality.
cout << "find, again!" << endl;
}
return 0;
}
/*
Yan
find
*/
当你看到输出情况后是否发现了问题?同样是find,作为成员函数的find是用基于等价的比较函数,而非成员函数则是用基于相等的比较函数,所以他们会出现不一样的结果。
从技术实现角度来讲,定义了 operator == 运算符的类型都属于Equality Comparable 。例如,C++中所有内置类型和指针类型都是Equality Comparable 概念下的类型。C++ STL中find、adjacent_find、find_first_of、search、find_end、search_n、count、 equal、mismatch、replace、replace_copy、remove、remove_copy、unique、 unique_copy等函数(如果有重载,均指非传入函数对象版本)要求元素类型属于Equality Comparable ,即要求该类型定义有 operator == 运算符。
从技术实现角度来讲,定义了 operator < 运 算符的类型 都属于Strict Weakly Comparable 。例如,C++中所有内置类型和指 针类型都是LessThan Comparable概念下的类型。C++ STL中next_permutation、prev_permutation、sort、stable_sort、partial_sort、 partial_sort_copy、nth_element、binary_search、lower_bound、upper_bound、 equal_range、merge、inplace_merge、includes、set_union、set_intersection、 set_difference、set_symmetric_difference、makde_heap、push_heap、pop_heap、 sort_heap、等函数(如果有重载,均指非传入函数对象版本),以及set、map、multiset、multimap、 priority_queue等容器类,都要求元素类型属于Strict Weakly Comparable ,即要求该类型定义有 operator < 运算符。
2. 为包含指针的关联容器指定比较类型
对于包含了指针的关联容器,几乎毫无疑问一定自己设置比较函数的子类,不然会出现意想不到的结果。另外需要注意的是,创建容器的参数都必须是类型,不能是函数,所以你一定放入一个函数对象,让STL在内部实现时调用这个函数。
此处直接给出正确的做法。不考虑内存泄露。
#include <iostream>
#include <set>
#include <string>
using namespace std;
struct Compare : public binary_function<const string*, const string*, bool> {
bool operator()(const string* s1, const string* s2) {
return *s1 < *s2;
}
};
int main(int argc, char *argv[]) {
set<string*, Compare> s;
s.insert(new string ("c"));
s.insert(new string ("b"));
s.insert(new string ("a"));
return 0;
}
然后,如果希望把容器内信息输出有几种不同的写法。
1 . 使用transform把容器内对象转换为引用然后输出给ostream_iterator.
struct Deference {
template<typename T>
const T& operator()(const T* ptr) const {
return *ptr;
}
};
transform(s.begin(), s.end(), ostream_iterator<string>(cout, "\n"), Deference());
2 . 使用for_each函数,然后用匿名函数。这里我发现原来匿名函数里不能使用auto,不然的话,就不需要判断容器内的类型了。
for_each(s.begin(), s.end(), [](string* s) {
cout << *s << endl;
});
3. 总是让对象的比较函数在相等时返回false
这是一个和小的细节,但是却会引发很大的问题。
例如在set中,每次输入都会比较要输入的值是否与原有的值等价,如此此时判断相等却给出了true,那么元素就会被插入进容器中,破坏容器中的数据结构。就算是在multiset也是一样会导致问题,例如在调用equal_range时,会因为比较函数而给出不正确的结果。
所以,如果你视图在比较函数中对相等的值返回true,你会破坏所有的标准关联容器,不管它是否保存相同的值。
4. 切勿直接修改set或muitiset中的键
请注意,map和muitimap在许多实现中key是const类型,是不允许被修改的,如果尝试修改他的键会直接导致不可移植性。但是对于set却不是这样的,他允许被修改,但必须注意的是,不能修改key。之所以不能修改set里面的key,是因为每次修改都会影响整个set的排列结构。
对于某些实现来说,set的key确实是const的。如果你非得要修改key,可以使用以下方法。
#include <iostream>
#include <set>
#include <string>
using namespace std;
class Employee {
public:
const string& name() const;
void setname(const string& name);
const string& title() const;
void settitle(const string& title);
};
struct names : public binary_function<Employee, Employee, bool> {
bool operator()(const Employee& lhs, const Employee& rhs) const {
return lhs.name() < rhs.name();
}
};
int main(int argc, char *argv[]) {
typedef set<Employee, names> EmpSet;
EmpSet se;
...
Employee selectTitle;
EmpSet::iterator i = se.find(selectTitle);
if (i != se.end()) {
// i->setname(...); 是不可移植的。
const_cast<Employee&> (*i).setname("yan");
}
return 0;
}
我们在这里设定set排序的key,并且尝试去改变他的key。需要注意的是const_cast<Employee&> (*i).setname("yan");
的写法。
通过改变他的const属性,把他转变为Employee&。 原因在于如果只是把他改为Employee
实际上只是获得他的一个复制对象,并不能改变他原来的对象。
至于如何修改map的键值。其实很简答。只要把原来的那个pair删除,重新加入一个新的修改了key的pair。
5. 当效率很重要时,请在map::operator[]和map::insert之间做出选择
map::operator[]和map::insert都能够用于更新和插入新的值,但是在效率上有很大的区别。
简单的说,map::operator[]适用于更新值。原因在于,
class Widget {
public:
Widget() {
cout << "constructed!" << endl;
}
Widget(double i) {
cout << "copy" << endl;
}
Widget& operator=(double i) {
cout << "pass" << endl;
return *this;
}
};
int main(int argc, char *argv[]) {
typedef map<int, Widget> IntWidgetMap;
IntWidgetMap m;
m[0] = 1.4;
return 0;
}
/*
constructed!
pass
*/
实际上是这样实现的:
class Widget {
public:
Widget() {
cout << "constructed!" << endl;
}
Widget(double i) {
cout << "copy" << endl;
}
Widget& operator=(double i) {
cout << "pass" << endl;
return *this;
}
};
int main(int argc, char *argv[]) {
typedef map<int, Widget> IntWidgetMap;
IntWidgetMap m;
pair<IntWidgetMap::iterator, bool> result = m.insert(IntWidgetMap::value_type(1, Widget()));
result.first->second = 1.50;
return 0;
}
/*
constructed!
pass
*/
而适用insert函数就高效许多。需要注意的是一定要使用value_type。这总是某种形式的pair。
int main(int argc, char *argv[]) {
typedef map<int, Widget> IntWidgetMap;
IntWidgetMap m;
m.insert(IntWidgetMap::value_type(1, 1.3));
return 0;
}
/*
copy
*/
而对于插入,我们总是倾向于适用operator[]。比较他们的函数调用即可知。
#include <iostream>
#include <map>
using namespace std;
class Widget {
public:
Widget() {
cout << "constructed!" << endl;
}
Widget(double i) {
cout << "copy" << endl;
}
Widget& operator=(double i) {
cout << "pass" << endl;
return *this;
}
};
int main(int argc, char *argv[]) {
typedef map<int, Widget> IntWidgetMap;
IntWidgetMap m;
m.insert(IntWidgetMap::value_type(1, 1.3));
cout << endl;
m[1] = 1.5;
cout << endl;
m.insert(IntWidgetMap::value_type(1, 1.5)).first->second = 10;
return 0;
}