Символьные характеристики являются чрезвычайно важным компонентом библиотек потоков и строк, поскольку они позволяют классам потоков / строк отделить логику , какие символы хранятся , от логики , какие манипуляциидолжны выполняться с этими символами.
Для начала, класс черт характера по умолчанию, char_traits<T>
, широко используется в стандарте C ++.Например, нет класса с именем std::string
.Скорее, существует шаблон класса std::basic_string
, который выглядит следующим образом:
template <typename charT, typename traits = char_traits<charT> >
class basic_string;
Тогда std::string
определяется как
typedef basic_string<char> string;
Аналогично, стандартные потоки определяются как
template <typename charT, typename traits = char_traits<charT> >
class basic_istream;
typedef basic_istream<char> istream;
Так почему же эти классы структурированы так, как они есть?Почему мы должны использовать класс странных признаков в качестве аргумента шаблона?
Причина в том, что в некоторых случаях нам может потребоваться строка, похожая на std::string
, но с некоторыми немного другими свойствами.Один классический пример этого - если вы хотите хранить строки таким образом, чтобы игнорировать регистр.Например, я мог бы захотеть сделать строку с именем CaseInsensitiveString
так, чтобы я мог иметь
CaseInsensitiveString c1 = "HI!", c2 = "hi!";
if (c1 == c2) { // Always true
cout << "Strings are equal." << endl;
}
То есть я могу иметь строку, в которой две строки, отличающиеся только чувствительностью к регистру, сравниваются равными.
Теперь предположим, что авторы стандартной библиотеки разработали строки без использования черт.Это означало бы, что в стандартной библиотеке у меня был бы очень мощный строковый класс, который был бы совершенно бесполезен в моей ситуации.Я не мог повторно использовать большую часть кода для этого строкового класса, так как сравнения всегда работали против того, как я хотел, чтобы они работали.Но используя черты, на самом деле можно повторно использовать код, который управляет std::string
, чтобы получить строку без учета регистра.
Если вы откроете копию стандарта C ++ ISO и посмотрите на определение того, какОператоры сравнения строк работают, вы увидите, что все они определены в терминах функции compare
.Эта функция, в свою очередь, определяется вызовом
traits::compare(this->data(), str.data(), rlen)
, где str
- строка, с которой вы сравниваете, а rlen
- меньшая из двух длин строки.Это на самом деле довольно интересно, потому что это означает, что определение compare
напрямую использует функцию compare
, экспортируемую по типу признаков, указанному в качестве параметра шаблона!Следовательно, если мы определим новый класс признаков, а затем определим compare
, чтобы он сравнивал символы без учета регистра, мы можем создать строковый класс, который ведет себя так же, как std::string
, но обрабатывает вещи без учета регистра!
Вот пример.Мы наследуем от std::char_traits<char>
, чтобы получить поведение по умолчанию для всех функций, которые мы не пишем:
class CaseInsensitiveTraits: public std::char_traits<char> {
public:
static bool lt (char one, char two) {
return std::tolower(one) < std::tolower(two);
}
static bool eq (char one, char two) {
return std::tolower(one) == std::tolower(two);
}
static int compare (const char* one, const char* two, size_t length) {
for (size_t i = 0; i < length; ++i) {
if (lt(one[i], two[i])) return -1;
if (lt(two[i], one[i])) return +1;
}
return 0;
}
};
(Обратите внимание, что я также определил eq
и lt
здесь, которые сравнивают символы дляравенство и меньше, соответственно, а затем определено compare
в терминах этой функции).
Теперь, когда у нас есть этот класс черт, мы можем определить CaseInsensitiveString
тривиально как
typedef std::basic_string<char, CaseInsensitiveTraits> CaseInsensitiveString;
И вуаля!Теперь у нас есть строка, которая обрабатывает все без учета регистра!
Конечно, есть и другие причины для использования черт.Например, если вы хотите определить строку, которая использует некоторый базовый тип символов фиксированного размера, вы можете специализировать char_traits
для этого типа, а затем создавать строки из этого типа.Например, в Windows API есть тип TCHAR
, который является либо узким, либо широким символом, в зависимости от того, какие макросы вы установили во время предварительной обработки.Затем вы можете сделать строки из TCHAR
s, написав
typedef basic_string<TCHAR> tstring;
И теперь у вас есть строка TCHAR
s.
Во всех этих примерах обратите внимание, что мы простоопределил некоторый класс признаков (или использовал уже существующий) в качестве параметра для некоторого типа шаблона, чтобы получить строку для этого типа.Весь смысл в том, что автору basic_string
нужно просто указать, как использовать черты, и мы волшебным образом можем заставить их использовать наши черты, а не значения по умолчанию, чтобы получить строки, которые имеют некоторый нюанс или причуду, не являющуюся частью строкового типа по умолчанию..
Надеюсь, это поможет!
РЕДАКТИРОВАТЬ : Как указывало @phooji, это понятие признаков используется не только STL, но и не специфично для C ++. Как бесстыдная самореклама, некоторое время назад я написал реализацию троичного дерева поиска (тип описанного здесь базового дерева ), который использует черты для хранения строк любого типа. и используя тот тип сравнения, который клиент хочет, чтобы он сохранил. Это может быть интересно прочитать, если вы хотите увидеть пример, где это используется на практике.
РЕДАКТИРОВАТЬ : В ответ на ваше заявление о том, что std::string
не использует traits::length
, оказывается, что оно используется в нескольких местах. В частности, когда вы создаете std::string
из строки char*
C-стиля, новая длина строки получается путем вызова traits::length
для этой строки. Кажется, что traits::length
используется главным образом для работы с последовательностями символов в стиле C, которые являются «наименее общим знаменателем» строк в C ++, тогда как std::string
используется для работы со строками произвольного содержимого.