Характеристики символов - чрезвычайно важный компонент библиотек потоков и строк, поскольку они позволяют классам потоков / строк отделять логику того, какие символы хранятся, от логики того, какие манипуляции должны выполняться с этими символами.
Начнем с того, что класс характеристик символов по умолчанию 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который является узким или широким символом в зависимости от того, какие макросы вы установили во время предварительной обработки. Затем вы можете сделать строки из TCHARs, написав
typedef basic_string<TCHAR> tstring;
И теперь у вас есть строка TCHARs.
Обратите внимание, что во всех этих примерах мы просто определили некоторый класс признаков (или использовали тот, который уже существует) как параметр для некоторого типа шаблона, чтобы получить строку для этого типа. Все дело в том, что basic_stringавтору просто нужно указать, как использовать черты, и мы волшебным образом можем заставить их использовать наши черты, а не стандартные, чтобы получить строки, которые имеют некоторые нюансы или причуды, не являющиеся частью типа строки по умолчанию.
Надеюсь это поможет!
РЕДАКТИРОВАТЬ : Как отметил @phooji, это понятие черт не только используется STL, но и не относится к C ++. Как совершенно бессовестная самореклама, некоторое время назад я написал реализацию тернарного дерева поиска (тип основанного на корне дерева, описанного здесь ), которое использует черты характера для хранения строк любого типа и с использованием любого типа сравнения, который клиент хочет их хранить. Это может быть интересное чтение, если вы хотите увидеть пример того, как это используется на практике.
РЕДАКТИРОВАТЬ : В ответ на ваше заявление, которое std::stringне используется traits::length, оказалось, что в нескольких местах он используется. В частности, когда вы строите std::stringотключения от char*строки C-стиле, новая длина строки определяется путем вызова traits::lengthна этой строке. Похоже, что traits::lengthон используется в основном для работы с последовательностями символов в стиле C, которые являются «наименьшим общим знаменателем» строк в C ++, в то время std::stringкак используется для работы со строками произвольного содержания.