{
return ciStringCompare(lhs,rhs); // Реализация ciStringCompare
// приведена в совете 35
}
};
При наличии CiStringCompare контейнер set<string>, игнорирующий регистр символов, создается очень просто:
set<string.CIStringCompare> ciss:
Теперь при вставке строк «Persephone» и «persephone» в множество будет включена только первая строка, поскольку вторая считается эквивалентной:
ciss.insert('Persephone'); // В множество включается новый элемент
ciss.insert('persephone'); // Эквивалентный элемент не включается
Если теперь провести поиск строки «persephone» функцией set::find, результат будет успешным:
if(ciss.find('persephone')!=ciss.end())... // Элемент найден
Но если воспользоваться внешним алгоритмом find, поиск завершается неудачей:
if(find(ciss.begin(),ciss.end(),
'persephone')!=ciss.end())... // Элемент отсутствует
Дело в том, что строка «persephone»
Возникает вопрос — почему же в работе стандартных ассоциативных контейнеров используется понятие эквивалентности, а не равенства? Ведь большинству программистов равенство кажется интуитивно более понятным, чем эквивалентность (в противном случае данный совет был бы лишним). На первый взгляд ответ кажется простым, но чем дольше размышляешь над этим вопросом, тем менее очевидным он становится.
Стандартные ассоциативные контейнеры сортируются, поэтому каждый контейнер должен иметь функцию сравнения (по умолчанию less), определяющую порядок сортировки. Эквивалентность определяется в контексте функции сравнения, поэтому клиентам стандартных ассоциативных контейнеров остается лишь задать единственную функцию сравнения. Если бы ассоциативные контейнеры при сравнении объектов использовали критерий равенства, то каждому ассоциативному контейнеру, помимо используемой при сортировке функции сравнения, пришлось бы определять вторую функцию для проверки равенства. Вероятно, по умолчанию функция сравнения вызывала бы equal_to, но интересно заметить, что функция equal_to в STL не используется в качестве стандартной функции сравнения. Когда в STL возникает необходимость проверки равенства, по умолчанию просто вызывается оператор =. В частности, именно так поступает внешний алгоритм find.
Допустим, у нас имеется контейнер set2CF, построенный по образцу set — «set с двумя функциями сравнения». Первая функция сравнения определяет порядок сортировки элементов множества, а вторая используется для проверки равенства. А теперь рассмотрим следующее объявление:
set2CF<string.CIStringCompare,equal_to<string> > s;
Контейнер s производит внутреннюю сортировку строк без учета регистра, но с использованием интуитивного критерия равенства: две строки считаются равными при совпадении их содержимого. Предположим, в s вставляются два варианта написания строки «Persephone»:
s.insert('Persephone');
s.insert('persephone');
Как поступить в этом случае? Если контейнер поймет, что 'Persephone' != 'persephone', и вставит обе строки в s, в каком порядке они должны следовать?
Напомню, что функция сортировки эти строки не различает. Следует ли вставить строки в произвольном порядке, добровольно отказавшись от детерминированного порядка перебора содержимого контейнера? Недетерминированный порядок перебора уже присущ ассоциативным контейнерам multiset и multimap, поскольку Стандарт не устанавливает никаких ограничений на относительный порядок следования эквивалентных значений (multiset) или ключей (multimap). Или нам следует настоять на детерминированном порядке содержимого s и проигнорировать вторую попытку вставки (для строки «persephone»)? А если будет выбран этот вариант, что произойдет при выполнении следующей команды:
if (s.find('persephone') != s.end())... // Каким будет результат проверки?
Функция find использует проверку равенства, но если проигнорировать второй вызов insert для сохранения детерминированного порядка элементов s, проверка даст отрицательный результат — хотя строка «persephone» была отвергнута как дубликат!
Мораль: используя одну функцию сравнения и принимая решение о «совпадении» двух значений на основании их эквивалентности, мы избегаем многочисленных затруднений, возникающих при использовании двух функций сравнения. Поначалу такой подход выглядит несколько странно (особенно когда вы видите, что внутренняя и внешняя версии find возвращают разные результаты), но в перспективе он избавляет от всевозможных затруднений, возникающих при смешанном использовании равенства и эквивалентности в стандартных ассоциативных контейнерах.
Но стоит отойти от
Совет 20. Определите тип сравнения для ассоциативного контейнера, содержащего указатели
Предположим, у нас имеется контейнер set, содержащий указатели string*, и мы пытаемся включить в него несколько новых элементов:
set<string*> ssp; // ssp = 'set of string ptrs'
ssp.insert(new string('Anteater'));
ssp.insert(new string('Wombat'));
ssp.insert(new string('Lemur'));
ssp.insert(new string('Penguin'));
Следующий фрагмент выводит содержимое set. Предполагается, что строки будут выведены в алфавитном порядке — ведь содержимое контейнеров set автоматически сортируется!
for (set<string*>::const_iterator i = ssp.begin(); // Предполагаемый
i!=ssp.end();
++i)
cout <<*i << endl;
Однако на практике ничего похожего не происходит. Вместо строк выводятся четыре шестнадцатеричных числа — значения указателей. Поскольку в контейнере set хранятся указатели, *i является не строкой, а
copy(ssp.begin(),ssp.end(),// Скопировать строки.
ostream_iterator<string>(cout,'
')); //содержащиеся в ssp. в cout
//(не компилируется!)
не только делает программу более компактной, но и помогает быстрее обнаружить ошибку, поскольку вызов сору не компилируется. Итератор ostream_iterator должен знать тип выводимого объекта, поэтому когда компилятор обнаруживает расхождение между заданным в параметре шаблона типом string и типом объекта, хранящегося в ssp (string*), он выдает ошибку. Еще один довод в пользу сильной типизации...
Если заменить *i в цикле на **i,
Подходя к решению этой проблемы, нелишне вспомнить, что объявление
set<string*> ssp;
представляет собой сокращенную запись для объявления
set<string*.less<string*> > ssp;
Строго говоря, это сокращенная запись для объявления
set<string*.less<string*>.allocator<string*> > ssp;
но в контексте данного совета распределители памяти несущественны.
Если вы хотите сохранить указатели string* в контейнере set так, чтобы их порядок определялся значениями строк, стандартный функтор сравнения less<string*> вам не подойдет. Вместо этого необходимо написать собственный функтор сравнения, который получает указатели string* и упорядочивает их по содержимому строк, на которые они ссылаются. Пример:
struct StringPtrLess:
public binary_function<const string*, // Базовый класс
const string*, // описан в совете 40
bool> {
bool operator() (const string *ps1, const string *ps2) const
{
return *ps1<*ps2:
}
};
.
После этого StringPtrLess используется в качестве типа критерия сравнения ssp: