В чем смысл черт характера STL?

84

Я заметил, что в моей копии справочника SGI STL есть страница о характеристиках характера, но я не вижу, как они используются? Заменяют ли они функции string.h? Кажется, что они не используются std::string, например, length()метод on std::stringне использует length()метод черт характера . Почему существуют Черты характера и используются ли они на практике?

Мэтью Смит
источник

Ответы:

172

Характеристики символов - чрезвычайно важный компонент библиотек потоков и строк, поскольку они позволяют классам потоков / строк отделять логику того, какие символы хранятся, от логики того, какие манипуляции должны выполняться с этими символами.

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

templatetypedef
источник
14
Похоже, вы отдали должное своему имени пользователя :) Возможно, это также актуально: многие библиотеки boost используют концепции и классы признаков типов, так что это не просто стандартная библиотека. Кроме того, аналогичные методы используются в других языках без использования шаблонов, см. Эзотерический пример: ocaml.janestreet.com/?q=node/11 .
phooji
2
хорошая структура (троичное дерево поиска), однако я бы отметил, что попытки можно "сжать" различными способами: 1 / с использованием диапазонов символов для указания на дочерний элемент, а не отдельных символов (выигрыш очевиден), 2 / сжатие пути (деревья Патрисии) и 3 / buckets в конце ветвей (т. е. просто используйте отсортированный массив строк, если их меньше K). Их объединение (я объединил 1 и 3) резко снижает потребление памяти, не влияя на быстродействие более чем на постоянный коэффициент (и фактически, сегменты уменьшают количество переходов).
Matthieu M.
2
@ dan04: Попробуйте использовать любой стандартный класс / алгоритм для использования вашей функции.
Xeo
2
Итак ... вкратце, трейты - это просто своего рода интерфейс, используемый классом basic_string для управления различными типами символов, независимо от того, какие они на самом деле, верно?
Virus721 02
1
@ Virus721 Черты - это скорее не реализации, "подключенные" к данному классу? То, что класс получает от трейта, - это, IMHO, реализация, а не интерфейс. Конечно, у трейта есть свой интерфейс.
dom_beau