Характеристики символов - чрезвычайно важный компонент библиотек потоков и строк, поскольку они позволяют классам потоков / строк отделять логику того, какие символы хранятся, от логики того, какие манипуляции должны выполняться с этими символами.
Начнем с того, что класс характеристик символов по умолчанию 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) {
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
как используется для работы со строками произвольного содержания.