關聯容器(第3章)(條款21,22)
條款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章