В совете 35 приведена реализация сравнения строк без учета регистра символов с применением алгоритмов mismatch и lexicographical_compare, но в нем также указано, что полноценное решение должно учитывать локальный контекст. Книга посвящена STL, а не вопросам интернационализации, поэтому локальным контекстам в ней не нашлось места. Тем не менее, Мэтт Остерн, автор книги «Generic Programming and the STL» [4], посвятил этой теме статью в майском номере журнала «С++ Report» [И]. Текст этой статьи приведен в настоящем приложении. Я благодарен Мэтту и фирме lOlcommunications за то, что они разрешили мне это сделать.
Сравнение строк без учета регистра символов
Мэтт Остерн
Если вам когда-либо доводилось писать программы, в которых используются строки (а кому, спрашивается, не доводилось?), скорее всего, вы встречались с типичной ситуацией — две строки, различающиеся только регистром символов, должны были интерпретироваться как равные. В этих случаях требовалось, чтобы операции сравнения — проверка равенства, больше-меньше, выделение подстрок, сортировка — игнорировали регистр символов. Программисты очень часто спрашивают, как организовать подобные операции средствами стандартной библиотеки С++. На этот вопрос существует огромное количество ответов, многие из которых неверны.
Прежде всего необходимо избавиться от мысли о написании класса, сравнивающего строки без учета регистра. Да, с технической точки зрения это более или менее возможно. Тип std:: string стандартной библиотеки в действительности является синонимом для типа std::basic_string
Вы не сможете выполнять операции ввода-вывода или это потребует больших дополнительных хлопот. Классы ввода-вывода стандартной библиотеки (такие как std:: basic_istream и std::basic_ostream) специализируются по двум начальным параметрам std::basic_string (а std::ostream всего лишь является синонимом для std::basic_ostreanKchar,char_traits
Игнорирование регистра символов является не свойством объекта, а лишь контекстом его использования. Вполне возможно, что в одном контексте строки должны интерпретироваться с учетом регистра, а в другом контексте регистр должен игнорироваться (например, при установке соответствующего режима пользователем).
Решение не соответствует канонам. Класс char_traits, как и все классы характеристик, прост, компактен и не содержит информации состояния. Как будет показано ниже, правильная реализация сравнений без учета регистра не отвечает ни одному из этих критериев.
Этого вообще не достаточно. Даже если все функции basic_string будут игнорировать регистр, это никак не отразится на использовании внешних обобщенных алгоритмов, таких как std::search и std::find_end. Кроме того, такое решение перестает работать, если по соображениям эффективности перейти от контейнера объектов basicstring к таблице строк.
Более правильное решение, которое лучше соответствует архитектуре стандартной библиотеки, заключается в том, чтобы игнорировать регистр символов только в тех случаях, когда это действительно необходимо. Не стоит возиться с такими функциями контейнера string, как string::find_first или string::rfind; они лишь дублируют функциональные возможности, уже поддерживаемые внешними обобщенными алгоритмами. С другой стороны, алгоритмы обладают достаточной гибкостью, что позволяет реализовать в них поддержку сравнений строк без учета регистра. Например, чтобы отсортировать коллекцию строк без учета регистра, достаточно передать алгоритму працильный объект функции сравнения:
std::sort(С.begin(), С.end().compare_wi thout_case);
Написанию таких объектов и посвящена эта статья.
Первая попытка
Существует несколько способов упорядочения слов по алфавиту. Зайдите в книжный магазин и посмотрите, как расставлены книги на полках. Предшествует ли имя
1 См. статью Александреску A. (Andrei Alexandrescu) в майском номере «С++ Report» за 2000 г. [19].
Mary McCarthy имени Bernard Malamud или следует после него? (В действительности это лишь вопрос привычки, я встречал оба варианта.) Впрочем, простейший способ сравнения строк хорошо знаком нам по школе: речь идет о лексикографическом, или «словарном», сравнении, основанном на последовательном сравнений отдельных символов двух строк.
Лексикографический критерий сравнения может оказаться неподходящим для некоторых специфических ситуаций. Более того, единого критерия вообще не существует — например, имена людей и географические названия иногда сортируются по разным критериям. С другой стороны, в большинстве случаев лексикографический критерий подходит, поэтому он был заложен в основу механизма строковых сравнений в С++. Строка представляет собой последовательность символов. Если объекты х и у относятся к типу std:: string, то выражение х<у эквивалентно выражению
std:: lexicographical_compare(x.begin(),x.end(), y.begin(), y.end())
В приведенном выражении алгоритм lexicographical_compare сравнивает отдельные символы оператором <, однако существует другая версия lexicographical_ compare, позволяющая задать пользовательский критерий сравнения символов. Она вызывается с пятью аргументами вместо четырех; в последнем аргументе передается объект функции, двоичный предикат, определяющий, какой из двух символов предшествует другому. Таким образом, для сравнения строк без учета регистра на базе lexicographical_compare достаточно объединить этот алгоритм с объектом функции, игнорирующим различия в регистре символов.
Распространенный принцип сравнения двух символов без учета регистра заключается в том, чтобы преобразовать оба символа к верхнему регистру и срац-нить результаты. Ниже приведена тривиальная формулировка этой идеи в виде объекта функции С++ с использованием хорошо известной функции toupper из стандартной библиотеки С:
struct lt_nocase
:public std::binary_function
bool operator() (char x.char y) const{
return std::toupper(static_cast
std::toupper(static_cast
}
};
«У каждой сложной задачи есть решение простое, элегантное и... неправильное» Авторы книг С++ обожают этот класс за простоту и наглядность. Я тоже неоднократно использовал его в своих книгах. Он почти правилен, и все-таки не совсем, хотя недостаток весьма нетривиален. Следующий пример выявляет этот недостаток:
int main() {
const char* si = "GEW\334RZTRAMINER";
const char* s2 = "gew\374rztraminer";
printf("sl=%s, s2=%s\n",s1,s2);
printf("sl
std: lexicographical_compare(s1,s1+14,s2,s2+14,lt_nocase())
?"true":"false"):
}
Попробуйте запустить эту программу в своей системе. На моем компьютере (Silicon Graphics О2 с системой IRIX 6.5) результат выглядел так:
sl=GEWURZTRAMINER,s2=gewQrztraminer
sl
Странно... Разве при сравнении без учета регистра «GEWURZTRAMINER» и «gewurztraminer» не должны быть равными? И еще возможен вариант с небольшой модификацией: если перед командой printf вставить строку
setlocale(LC_ALL,"de");
результат неожиданно изменяется:
sl=GEW0RZTRAMINER,s2=gewurztraminer
sl
Задача сравнения строк без учета регистра сложнее, чем кажется сначала. Работа элементарной на первый взгляд программы в огромной степени зависит от того, о чем многие из нас предпочли бы забыть. Речь идет о локальном контексте.
Локальный контекст
Символьный тип char в действительности представляется самым обычным целым числом. Это число можно интерпретировать как символ, но такая интерпретация ни в коем случае не является универсальной. Что должно соответствовать конкретному числу — буква, знак препинания, непечатаемый управляющий символ?
На этот вопрос невозможно дать однозначный ответ. Более того, с точки зрения базовых языков С и С++ различия между этими категориями символов не так уж существенны и проявляются лишь в некоторых библиотечных функциях: например, функция isalpha проверяет, является ли символ буквой, а функция toupper переводит символы нижнего регистра в верхний регистр и оставляет без изменений буквы верхнего регистра и символы, не являющиеся буквами. Подобная классификация символов определяется особенностями культурной и лингвистической среды. В английском языке действуют одни правила, по которым буквенные символы отличаются от «не буквенных», в шведском — другие и т. д. Преобразование из нижнего регистра в верхний имеет один смысл в латинском алфавите, другой — в кириллице, и вообще не имеет смысла в иврите.
По умолчанию функции обработки символов работают с кодировкой, подходящей для простого английского текста. Символ '\374' не изменяется функцией toupper, поскольку он не считается буквой; в некоторых системах при выводе он имеет вид ü, но для библиотечной функции С, работающей с английским текстом, это несущественно. В кодировке ASCII нет символа ü. Команда
setlocale(LC_ALL,"de"):
сообщает библиотеке С о переходе на немецкие правила (по крайней мере в системе IRIX — имена локальных контекстов не стандартизованы). В немецком языке есть символ ü, поэтому функция toupper преобразует ü в Ü.
У любого нормального программиста этот факт вызывает обоснованное беспокойство. Оказывается, простая функция toupper, вызываемая с одним аргументом, зависит еще и от глобальной переменной — хуже того, от скрытой глобальной переменной. Это приводит к стандартной проблеме: на работу функции, использующей toupper, теоретически может повлиять любая другая функция во всей программе.
При использовании toupper для сравнения строк без учета регистра результат может быть катастрофическим. Предположим, у вас имеется алгоритм, получающий отсортированный, список (скажем, binary_search); все работает нормально, как вдруг новый локальный контекст на ходу изменяет порядок сортировки. Такой код не подходит для многократного использования. Более того, он вообще едва ли способен принести практическую пользу. Его нельзя применить в библиотеке — библиотеки используются множеством разных программ, не только теми, которые никогда не вызывают функцию setlocalе. Возможно, вам удастся применить его в какой-нибудь большой программе, но это приводит к проблемам сопровождения. Возможно, вам удастся проследить за тем, чтобы все остальные модули не вызывали setlocalе, но как предотвратить вызов setlocalе модулем, который появится только в следующей версии программы?
В языке С приемлемого решения этой проблемы не существует. Библиотека С использует единственный локальный контекст, и с этим ничего не поделаешь. Решение существует в языке С++.
Локальные контексты в С++
В стандартной библиотеке С++ локальный контекст не является глобальной структурой данных, запрятанной где-то в недрах реализации библиотеки. Это объект типа std::locale, который можно создать и передать его другой функции, как любой другой объект. Пример создания объекта для стандартного локального контекста:
std::locale L = std::locale::classic():
Локальный контекст немецкого языка создается командой
std::locale L("de");
Имена локальных контекстов, как и в библиотеке С, не стандартизованы. Список имен локальных контекстов, доступных в вашей реализации, следует искать в документации.
Локальные контексты С++ делятся на фасеты (facets), связанные с разными аспектами интернационализации. Для извлечения заданного фасета из объекта локального контекста используется функция std:: use_facet. Фасет ctype отвечает за классификацию символов, в том числе и преобразования типа. Если c1 и с2 относятся к типу char, следующий фрагмент сравнивает их без учета регистра по правилам локального контекста L.
const std::ctype
bool result = ct-toupper(cl)
Предусмотрена особая сокращенная запись: std:: toupper (с, L), эквивалентная
std::use_facet
Использование use_facet стоит свести к минимуму, поскольку оно связано с заметными затратами.
По аналогии с тем, как лексикографическое сравнение оказывается неподходящим в некоторых ситуациях, преобразования символов «один-в-один» тоже годятся не всегда (например, в немецком языке символ (3 нижнего регистра соответствует последовательности «SS» в верхнем регистре). К сожалению, средства преобразования регистра в стандартных библиотеках С и С++ работают только с отдельными символами. Если это ограничение вас не устраивает, решение со стандартными библиотеками отпадает.
Фасет collate
Если вы знакомы с локальными контекстами С++, возможно, вам уже пришел в голову другой способ сравнения строк. У фасета collate, предназначенного для инкапсуляции технических аспектов сортировки, имеется функция, по интерфейсу весьма близкая к библиотечной функции С strcmp. Существует даже специальное средство, упрощающее сравнение двух строк: для объекта локального контекста L строки х и у могут сравниваться простой записью L(x,y), что позволяет обойтись без хлопот, связанных с вызовом use_facet и функции collate.
«Классический» локальный контекст содержит фасет collate, который выполняет лексикографическое сравнение по аналогии с функцией operator< контейнера string, но другие локальные контексты выполняют сравнение, руководствуясь своими критериями. Если в системе существует локальный контекст, обеспечивающий сравнение строк без учета регистра для интересующих вас языков, воспользуйтесь им. Возможно, этот локальный контекст даже не будет ограничиваться простым сравнением отдельных символов!
К сожалению, какой бы справедливой ни была эта рекомендация, она никак не поможет тем, у кого нет таких локальных контекстов. Возможно, когда-нибудь в будущем стандартное множество таких локальных контекстов будет стандартизировано, но сейчас никаких стандартов не существует. Если функция сравнения без учета регистра для вашей системы еще не написана, вам придется сделать это самостоятельно.
Сравнение строк без учета регистра
Фасет ctype позволяет относительно легко организовать сравнение строк без учета регистра на основе сравнения отдельных символов. Приведенная ниже версия не оптимальна, но по крайней мере она верна. В ней используется практически тот же принцип, что и прежде: строки сравниваются алгоритмом lexicographical_ compare, а отдельные символы сравниваются после приведения к верхнему регистру. Впрочем, на этот раз вместо глобальной переменной используется объект локального контекста. Кстати говоря, сравнение после приведения обоих символов к верхнему регистру не всегда дает тот же результат, что и после приведения к нижнему регистру. Например, во французском языке в символах верхнего регистра принято опускать диакритические знаки, вследствие чего вызов toupper во французском локальном контексте может приводить к потере информации: символы 'ё' и 'е' преобразуются в один символ верхнего регистра 'Е'. В этом случае при сравнении на базе функции toupper символы 'ё' и 'е' будут считаться одинаковыми, а при сравнении на базе tolower они будут считаться разными. Какой из ответов правилен? Вероятно, второй, но на самом деле все зависит от языка, национальных обычаев и специфики приложения.
struct lt_str_l
:public std::binary_function
struct lt_char{
const std::ctype
lt_char(const std::ctype
bool operator() (char x. char y) const {
return ct.toupper(x)
}
};
std::locale loc;
const std::ctype
lt_str_l(const std::locale& L = std::locale::classic())
:loc(L),ct(std::use_facet
bool operator()(const std::string& x, const std::string& y) const {
return std::lexicographical_compare(x.begin(),.x.end(),
y.begin(),y.end(), lt_char(ct));
}
};
Данное решение не оптимально; оно работает медленнее, чем могло бы работать. Проблема чисто техническая: функция toupper вызывается в цикле, а Стандарт С++ требует, чтобы эта функция была виртуальной. Некоторые оптимизаторы выводят вызов виртуальной функции из цикла, но чаще этого не происходит. Циклические вызовы виртуальных функций нежелательны.
В данном случае тривиального решения не существует. Возникает соблазнительная мысль — воспользоваться одной из функций объекта ctype:
const char* ctype
Эта функция изменяет регистр символов в интервале [f,i]. К сожалению, для наших целей этот интерфейс не подходит. Чтобы воспользоваться этой функцией для сравнения двух строк, необходимо скопировать обе строки в буферы и затем преобразовать их содержимое к верхнему регистру. Но откуда возьмутся эти буферы? Они не могут быть массивами фиксированного размера (неизвестно, каким должен быть максимальный размер), а динамические массивы потребуют дорогостоящего выделения памяти.
Альтернативное решение заключается в однократном преобразовании каждого символа с кэшированием результата. Такое решение недостаточно универсально—в частности, при использовании 32-разрядных символов UCS-4 оно абсолютно неработоспособно. С другой стороны, при работе с типом char (8-разрядным в большинстве систем) идея хранения 256 байт дополнительных данных в объекте функции сравнения выглядит вполне реально.
struct lt_str_2:
public std::binary_function
struct lt_char{
const char* tab;
lt_char(const char* t):tab(t) {}
bool operator() (char x, char y) const {
return tab[x-CHAR_MIN] < tab[y-CHAR-MIN];
}
};
char tab[CHAR_MAX-CHAR_MIN+l];
lt_str_2(const std::locale& L = std:.-locale::classic()){
const std::ctype
for(int i = CHAR_MIN;i<=CHAR_MAX;++i) tab[i-CHAR_MIN]=(char)i;
ct.toupper(tab. tab+(CHAR_MAX-CHAR_MIN+1));
}
bool operator()(const std::string& x. const std::string& y) const {
return std::lexicographical_compare(x.begin(),x.end(),
y.begin(),y.end(), lt_char(tab));
}
}
Как видите, различия между lt_str_1 и lt_str_2 не так уж велики. В первом случае используется объект функции сравнения символов, использующий фасет ctype напрямую, а во втором случае — объект функции сравнения с таблицей заранее вычисленных преобразований символов к верхнему регистру. Второе решение уступает первому, если создать объект функции lt_str_2, воспользоваться им для сравнения нескольких коротких строк и затем уничтожить. С другой стороны, при обработке больших объемов данных lt_str_2 работает значительно быстрее lt_str_1. В моих тестах превосходство было более чем двукратным: при использовании lt_str_l сортировка списка из 23 791 слова заняла 0,86 секунды, а при использовании lt_str_2 понадобилось только 0,4 секунды.
Итак, что же мы узнали?
•Класс строки без учета регистра символов реализуется на неправильном уровне абстракции. Обобщенные алгоритмы стандартной библиотеки С++ параметризуются, и этот факт следует использовать.
•Лексикографическое сравнение строк осуществляется сравнением отдельных символов. Если у вас имеется объект функции, сравнивающий символы без учета регистра, задача фактически решена, а этот объект может использоваться для сравнения других типов последовательностей символов, таких как vector
•Задача сравнения строк без учета регистра сложнее, чем кажется на первый взгляд. Она имеет смысл лишь в конкретном локальном контексте, поэтому объект функции сравнения должен содержать информацию о текущем локальном контексте. Если сравнение должно быть оптимизировано по скорости, напишите объект функции таким образом, чтобы избежать многократного вызова дорогостоящих операций с фасетами.
Правильное сравнение строк без учета символов требует большого объема рутинной работы, но ее необходимо проделать только один раз. Или вам, как и большинству коллег, не хочется думать о локальных контекстах? Впрочем, лет десять назад никому не хотелось думать об «ошибке 2000 года». И все же у вас больше шансов обойти стороной эту проблему, если ваш локально-зависимый код будет с самого начала правильно работать.