Риси символів є надзвичайно важливим компонентом бібліотек потоків та рядків, оскільки вони дозволяють класам потоків / рядків відокремлювати логіку того, які символи зберігаються, від логіки того, які маніпуляції слід виконувати з цими символами.
Для початку клас стандартних рис характеру,, 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) {
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
який є або вузьким, або широким символом, залежно від того, які макроси ви встановили під час попередньої обробки. Потім можна скласти рядки з TCHAR
s, написавши
typedef basic_string<TCHAR> tstring;
І тепер у вас є рядок TCHAR
s.
У всіх цих прикладах зауважте, що ми щойно визначили клас ознак (або використали вже існуючий) як параметр для якогось типу шаблону, щоб отримати рядок для цього типу. Вся суть цього в тому, що basic_string
автору просто потрібно вказати, як використовувати ознаки, і ми магічно можемо змусити їх використовувати наші риси, а не за замовчуванням, щоб отримати рядки, які мають якийсь нюанс або химерність, що не є частиною типу рядків за замовчуванням.
Сподіваюся, це допомагає!
EDIT : Як @phooji зазначив, це поняття чорт не тільки використовується STL, а також не специфічні для C ++. Як абсолютно безсоромне саморекламування, деякий час тому я написав реалізацію трійкового дерева пошуку ( тут описано тип дерева radix ), яке використовує ознаки для зберігання рядків будь-якого типу та використовуючи будь-який тип порівняння, який клієнт хоче, щоб вони зберігали. Це може бути цікаве читання, якщо ви хочете побачити приклад того, як це використовується на практиці.
EDIT : У відповідь на вашу претензію, std::string
яка не використовується traits::length
, виявляється, що вона використовується в кількох місцях. Зокрема, коли ви будуєте std::string
відключення від char*
рядка C-стилі, нова довжина рядка визначається шляхом виклику traits::length
на цьому рядку. Здається, traits::length
він використовується в основному для роботи з послідовностями символів у стилі С, які є "найменш загальним знаменником" рядків у C ++, тоді std::string
як використовується для роботи з рядками довільного вмісту.