Скотт Мейерс - Эффективное использование STL
struct StringPtrGreater:
public binary_function<const string*, // Жирным шрифтом выделены
const string*, // изменения, внесенные в код bool> {// из совета 20.
// Внимание - приведенное решение
// не работает!
bool operator()(const string *psl. const string *ps2) const
{
return !(*psl<*ps2); // Простое логическое отрицание
}// старого условия не работает!
};
Идея заключается в том, чтобы изменить порядок сортировки логическим отрицанием условия в функции сравнения. К сожалению, отрицанием операции «<» является не «>», а «>=», а мы выяснили, что операция «>=», возвращающая true для равных значений, не подходит для функции сравнения в ассоциативных контейнерах.
Правильный тип сравнения должен выглядеть так:
struct StringPtrGreater: // Правильный тип сравнения
public binary_function<const string*, // для ассоциативных контейнеров
const string*,
bool> {
bool operator() (const string *psl, const string *ps2) const {
return *ps2<*psl:// Поменять местами операнды
}
}:
Чтобы не попасть в ловушку, достаточно запомнить, что возвращаемое значение функции сравнения указывает, должно ли одно значение предшествовать другому в порядке сортировки, определяемом этой функцией. Равные значения никогда не предшествуют друг другу, поэтому функция сравнения всегда должна возвращать для них false.
Я знаю, о чем вы думаете. «Конечно, это имеет смысл для set и map, поскольку эти контейнеры не могут содержать дубликатов. А как насчет multiset и multimap? Раз эти контейнеры могут содержать дубликаты, так ли важно, что два объекта с одинаковыми значениями окажутся не эквивалентными? Сохраним оба, для этого и нужны mult-контейнеры. Верно?»
Нет, неверно. Давайте вернемся к исходному примеру, но на этот раз воспользуемся контейнером multiset:
multiset<int.less_equal<int> > s; // s сортируется по критерию "<="
s.insert(10):// Вставка числа 10А
s.insert(10);// Вставка числа 10в
Теперь s содержит два экземпляра числа 10, и было бы логично предположить, что при вызове equal _range мы получим пару итераторов, описывающих интервал с обеими копиями. Однако это невозможно. Функция equal_range, несмотря на свое название, определяет интервал не равных, а эквивалентных значений. В нашем примере функция сравнения s говорит, что 10А и 10в не эквивалентны, поэтому они не могут оказаться в интервале, определяемом функцией equal range.
Ну что, убедились? Функция сравнения всегда должна возвращать false для равных величин, в противном случае нарушается работа всех стандартных ассоциативных контейнеров (независимо от того, могут они содержать дубликаты или нет).
Строго говоря, функции сравнения, используемые для сортировки ассоциативных контейнеров, должны определять для сравниваемых объектов порядок строгой квазиупорядоченности (strict weak ordering); аналогичное ограничение действует и для функций сравнения, передаваемых алгоритмам, — таким, как sort (см. совет 31). Если вас заинтересуют подробности того, что понимается под строгой квазиупорядоченностью, информацию можно найти во многих серьезных руководствах по STL, в том числе «The С++ Standard Library» [3], «Generic Programming аnd the STL» [4] и на web-сайте SGI, посвященном STL [21]. Особых откровений не ждите, но одно из требований строгой квазиупорядоченности относится непосредственно к данному совету. Требование заключается в следующем: функция, определяющая строгую квазиупорядоченность, должна возвращать false при получении двух копий одного значения.
Совет 22. Избегайте изменения ключа «на месте» в контейнерах set и multiset
Понять смысл этого совета нетрудно. Контейнеры set/multiset, как и все стандартные ассоциативные контейнеры, хранят свои элементы в отсортированном порядке, и правильное поведение этих контейнеров зависит от сохранения этого порядка. Если изменить значение элемента в ассоциативном контейнере (например заменить 10 на 1000), новое значение окажется в неправильной позиции, что нарушит порядок сортировки элементов в контейнере.
Сказанное прежде всего касается контейнеров map и multimap, поскольку программы, пытающиеся изменить значение ключа в этих контейнерах, не будут компилироваться:
map<int.string> m;
m.begin()->first = 10:// Ошибка! Изменение ключей
// в контейнере map запрещено
multimap<int.string> mm;
mm.begin()->first = 20;// Ошибка! Изменение ключей
// в контейнере multimap запрещено
Дело в том, что элементы объекта типа map<K,V> или multimap<K,V> относятся к типу pair<const К, V>. Ключ относится к типу const К и поэтому не может изменяться. Впрочем, его все же можно изменить с применением const_cast, как показано ниже. Хотите — верьте, хотите — нет, но иногда это даже нужно.
Обратите внимание: в заголовке этого совета ничего не сказано о контейнерах пир и multimap. Для этого есть веские причины. Как показывает предыдущий пример, модификация ключа «на месте» невозможна для map и multimap (без применения преобразования const_cast), но может быть допустима для set и multiset. Для объектов типа set<T> и multiset<T> в контейнере хранятся элементы типа Т, а не const Т. Следовательно, элементы контейнеров set и multiset можно изменять в любое время, и преобразование const_cast для этого не требуется (вообще говоря, дело обстоит не так просто, но не будем забегать вперед).
Сначала выясним, почему элементы set и multiset не имеют атрибута const. Допустим, у нас имеется класс Emplоуее:
class Employee {
public:
const string& name() const;// Возвращает имя работника
void setName(const string& name); // Задает имя работника
const string& title() const;// Возвращает должность
void setTitle(const string& title); // Задает должность
int idNumber() const;// Возвращает код работника
}
Объект содержит разнообразные сведения о работнике. Каждому работнику назначается уникальный код, возвращаемый функцией idNumber. При создании контейнера set с объектами Emplоуее было бы вполне разумно упорядочить его по кодам работников:
struct IDNumberLess:
public binary_function<Employee.Employee,bool> { // См. совет 40
bool operator() (const Employees Ihs,
const Employees rhs) const
{
return lhs.idNumber() < rhs. IdNumber();
}
}
typedef set<Employee.IDNumberLess> EmplIDSet;
EmplIDSet se;// Контейнер set объектов
// Employee, упорядоченных
// по коду
С практической точки зрения код работника является ключом для элементов данного множества, а остальные данные вторичны. Учитывая это обстоятельство, ничто не мешает перевести работника на более интересную должность. Пример:
Employee selectedID;// Объект работника с заданным кодом
EmpIDSet::iterator =se.find(selectedlD);
if (i!=se.end()){
i->setTitle("Corporate Dety"); // Изменить должность
}
Поскольку мы всего лишь изменяем вторичный атрибут данных, не влияющий на порядок сортировки набора, этот фрагмент не приведет к порче данных, и он вполне допустим.
Спрашивается, почему нельзя применить ту же логику к ключам контейнеров map и multimap? Почему бы не создать контейнер map, ассоциирующий работников со страной, в которой они живут; контейнер с функцией сравнения IDNumberLess, как в предыдущем примере? И почему бы в таком контейнере не изменить должность без изменения кода работника, как в предыдущем примере?
Откровенно говоря, мне это кажется вполне логичным, однако мое личное мнение в данном случае несущественно. Важно то, что Комитет по стандартизации решил, что ключи map/multimap должны быть неизменными (const), а значения set/ multiset — не должны.
Значения в контейнерах set/multiset не являются неизменными, поэтому попытки их изменения обычно нормально компилируются. Данный совет лишь напоминает вам о том, что при модификации элементов set/multiset не следует изменять ключевую часть (то есть ту часть элемента, которая влияет на порядок сортировки в контейнере). В противном случае целостность данных контейнера будет нарушена, операции с контейнером начнут приводить к непредсказуемым результатам, и все это произойдет по вашей вине. С другой стороны, это ограничение относится только к ключевым атрибутам объектов, содержащихся в контейнере. Остальные атрибуты объектов находятся в вашем полном распоряжении — изменяйте на здоровье!
Впрочем, не все так просто. Хотя элементы set/multiset и не являются неизменными, реализации могут предотвратить их возможную модификацию. Например, оператор* для set<T>: iterator может возвращать const Т&, то есть результат разыменования итератора set может быть ссылкой на const-элемент контейнера! При такой реализации изменение элементов set и multiset невозможно, поскольку при любом обращении к элементу автоматически добавляется объявление const.
Законны ли такие реализации? Может, да, а может — нет. По этому вопросу Стандарт высказывается недостаточно четко, и в соответствии с законом Мерфи разные авторы интерпретируют его по-разному. В результате достаточно часто встречаются реализации STL, в которых следующий фрагмент компилироваться не будет (хотя ранее говорилось о том, что он успешно компилируется):