条款21:总是让比较函数在等值情况下返回false
很显然,相等的值不存在前后关系,10 < 10肯定是不对的,但是10 <= 10是对的,但是,切记不要讲less_equla用来当做关联容器的比较类型,我想现实中应该也没有人这么做。。。如果真有人这么干了,会发生什么呢
根据等价规则: !(10 <= 10) && !(10 <= 10),则是 !(true) && !(true), 那就是false,难道可以往set中插入两个10??? 毕竟等价规则判断两个10不等价。
即使在multiset中也不可以,10 和10不等价,那使用equal_range指定的区间中,10和10就不能出现在一起了??这肯定不是你想要的,所以,别这么干,切记不要用less_qual作为关联容器的比较类型。
必须“严格的弱序化”
条款22:切勿直接修改set或者multiset中的键
在说 set/multiset之前,我们先看看 map/multimap, const Key,标准规定了,使你无法修改(当然,你一定要改也是有办法的,但是那不是明智的选择)
typedef pair<const Key, T> value_type;
template < class Key, // map::key_type
class T, // map::mapped_type
class Compare = less<Key>, // map::key_compare
class Alloc = allocator<pair<const Key,T> > // map::allocator_type
> class map;
而 set/multiset 标准中没有这样的限制
template < class T, // set::key_type/value_type
class Compare = less<T>, // set::key_compare/value_compare
class Alloc = allocator<T> // set::allocator_type
> class set;
那我们尝试修改看看,定义一个员工类,以员工唯一的员工编号作为排序规则,目的是员工的其他内容可以修改,很显然,一个员工的职级可以调整,很遗憾,我所使用的G++是无法通过编译的,也许其他编译器可以,那也不具有移植性。
不过我们可以通过强制类型转换来修改,这样可以保证在不同的编译器之间都可以使用(然后这样真的好么,肯定不好)。对于员工类型来说,没有修改排序用的键值部分,看起来没什么影响,然而,对于int,容器中的元素已经没有顺序性了,这显然不是我们所希望看到的。
// 员工类
class Employee {
public:
Employee(int i) : mId(i), mTitle("") {
std::cout << "Employee construct..." << std::endl;
}
Employee(const Employee &other) : mId(other.id()), mTitle(other.title()) {
std::cout << "Employee copy construct..." << std::endl;
}
~Employee() {
std::cout << "Employee destruct..." << std::endl;
}
int id() const {
return mId;
}
const string &title() const {
return mTitle;
}
void setTitle(const string &title) {
mTitle = title;
}
private:
int mId;
string mTitle;
};
// 员工类的比较类型
struct IDLess {
bool operator()(const Employee &lhs, const Employee &rhs) const {
return lhs.id() < rhs.id();
}
};
void test_22() {
// int类型set尝试修改
set<int> iset{1, 2, 3, 4, 5};
auto iter = iset.find(2);
if (iter != iset.end()) {
// *iter = 20; // 无法通过编译,返回的迭代器是const类型
const_cast<int &>(*iter) = 10; // 强制类型转换,对返回的const引用去掉const属性后修改
}
// 员工类型set尝试修改非键值部分
set<Employee, IDLess> eset;
Employee e1(1); // 构造,稍后析构
eset.insert(e1); // 拷贝构造1
eset.insert(Employee(2)); // 临时对象构造2,拷贝构造到容器2,析构临时对象2
eset.emplace(3); // 直接在容器中构造3
eset.emplace_hint(eset.find(3), 4); // 临时对象构造用于查找3,临时对象析构3,直接在容器中构造4
eset.insert(std::move(Employee(5))); // 本来是想构造的对象直接移动到容器中,奈何容器没有提供move版本的insert,并没用
auto iter1 = eset.find(4);
if (iter1 != eset.end()) {
// iter1->setTitle(); // 错误,不能修改,返回的迭代器是const类型
const_cast<Employee &>(*iter1).setTitle("Corporate Deity"); // 强制类型转换,对返回的const引用去掉const属性后修改
}
return;
}
看看这个修改后奇怪的int类型set
即使如 Employee 类型的修改没有触及键值部分,这也是不提倡的,引出本条款的最佳实践。
stet1:找到需要修改的元素;
step2:做一个备份;
step3:修改备份的副本;
step4:将元素从容器中删除;
step5:把修改后的备份元素从新插入到容器中,根据刚才找到的位置(常数时间),如果不提示位置,就是对数时间;
最佳实践例子
void test_22() {
// int类型set尝试修改
set<int> iset{1, 2, 3, 4, 5};
auto iter = iset.find(2);
if (iter != iset.end()) {
// *iter = 20; // 无法通过编译,返回的迭代器是const类型
// const_cast<int &>(*iter) = 10; // 强制类型转换,对返回的const引用去掉const属性后修改
// 最佳实践
int cp = *iter;
cp = 20;
iset.erase(iter++);
iset.insert(iter, cp); // 当然,这个提示其实没有什么用,仅是展示
}
// 员工类型set尝试修改非键值部分
set<Employee, IDLess> eset;
Employee e1(1); // 构造,稍后析构
eset.insert(e1); // 拷贝构造1
eset.insert(Employee(2)); // 临时对象构造2,拷贝构造到容器2,析构临时对象2
eset.emplace(3); // 直接在容器中构造3
eset.emplace_hint(eset.find(3), 4); // 临时对象构造用于查找3,临时对象析构3,直接在容器中构造4
eset.insert(std::move(Employee(5))); // 本来是想构造的对象直接移动到容器中,奈何容器没有提供move版本的insert,并没用
auto iter1 = eset.find(4);
if (iter1 != eset.end()) {
// iter1->setTitle(); // 错误,不能修改,返回的迭代器是const类型
// const_cast<Employee &>(*iter1).setTitle("Corporate Deity"); // 强制类型转换,对返回的const引用去掉const属性后修改
// 最佳实践
Employee cp(*iter1);
cp.setTitle("Manager");
eset.erase(iter1++);
eset.insert(iter1, cp);
}
return;
}
参考:《Effective STL中文版》第3章