У чому сенс рис характеру STL?


83

Я помічаю, що в моїй копії посилання SGI STL є сторінка про Риси характеру, але я не бачу, як вони використовуються? Чи замінюють вони функції string.h? Здається, вони не використовуються std::string, наприклад, length()метод на std::stringне використовує length()метод Риси характеру . Чому існують Риси характеру і чи використовуються вони коли-небудь на практиці?

Відповіді:


171

Риси символів є надзвичайно важливим компонентом бібліотек потоків та рядків, оскільки вони дозволяють класам потоків / рядків відокремлювати логіку того, які символи зберігаються, від логіки того, які маніпуляції слід виконувати з цими символами.

Для початку клас стандартних рис характеру,, char_traits<T>широко використовується у стандарті С ++. Наприклад, немає класу, який називається 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на цьому типі, а потім створювати рядки з цього типу. Наприклад, в API Windows є тип, TCHARякий є або вузьким, або широким символом, залежно від того, які макроси ви встановили під час попередньої обробки. Потім можна скласти рядки з TCHARs, написавши

typedef basic_string<TCHAR> tstring;

І тепер у вас є рядок TCHARs.

У всіх цих прикладах зауважте, що ми щойно визначили клас ознак (або використали вже існуючий) як параметр для якогось типу шаблону, щоб отримати рядок для цього типу. Вся суть цього в тому, що basic_stringавтору просто потрібно вказати, як використовувати ознаки, і ми магічно можемо змусити їх використовувати наші риси, а не за замовчуванням, щоб отримати рядки, які мають якийсь нюанс або химерність, що не є частиною типу рядків за замовчуванням.

Сподіваюся, це допомагає!

EDIT : Як @phooji зазначив, це поняття чорт не тільки використовується STL, а також не специфічні для C ++. Як абсолютно безсоромне саморекламування, деякий час тому я написав реалізацію трійкового дерева пошуку ( тут описано тип дерева radix ), яке використовує ознаки для зберігання рядків будь-якого типу та використовуючи будь-який тип порівняння, який клієнт хоче, щоб вони зберігали. Це може бути цікаве читання, якщо ви хочете побачити приклад того, як це використовується на практиці.

EDIT : У відповідь на вашу претензію, std::stringяка не використовується traits::length, виявляється, що вона використовується в кількох місцях. Зокрема, коли ви будуєте std::stringвідключення від char*рядка C-стилі, нова довжина рядка визначається шляхом виклику traits::lengthна цьому рядку. Здається, traits::lengthвін використовується в основному для роботи з послідовностями символів у стилі С, які є "найменш загальним знаменником" рядків у C ++, тоді std::stringяк використовується для роботи з рядками довільного вмісту.


15
+ ∞
Chris Lutz

14
Здається, ви довели справедливість до свого імені користувача :) Можливо, це також актуально: багато бібліотек імпульсу використовують концепції та класи ознак типу, тож це не просто стандартна бібліотека. Крім того, подібні методи використовуються в інших мовах без використання шаблонів, див. Езотеричний приклад: ocaml.janestreet.com/?q=node/11 .
phooji

2
приємна структура (тернарне дерево пошуку), однак я б зазначив, що спроби можна "ущільнювати" різними способами: 1 / використовуючи діапазони символів, щоб вказувати на дочірню, а не окремі символи (виграш очевидний), 2 / стиснення шляху (дерева Patricia) та 3 / відра в кінці гілок (тобто просто використовуйте відсортований масив рядків, якщо їх менше ніж K). Поєднання цих (я поєднав 1 і 3) різко зменшує споживання пам'яті, не впливаючи на швидкість роботи більш ніж на постійний коефіцієнт (і насправді сегменти зменшують кількість стрибків).
Matthieu M.

2
@ dan04: Спробуйте отримати будь-який стандартний клас / алгоритм для використання вашої функції.
Xeo

2
Отже ... якщо коротко сказати це, риси - це просто якийсь інтерфейс, що використовується класом basic_string для маніпулювання різними типами символів, незалежно від того, що вони є, правда?
Virus721
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.