собственный класс string
. Такая идея редко хороша, поскольку не так просто безопасно обработать строки. К счастью, класс std::string
— лишь специализированное ключевое слово шаблонного класса std::basic_string
. Данный класс содержит все сложные средства обработки памяти, но не навязывает никаких правил, как копировать строки, сравнивать их и т.д. Эти правила импортируются в basic_string
, для чего принимается шаблонный параметр, который содержит класс traits
.В этом примере мы увидим, как создавать собственные классы типажей и, соответственно, как создавать пользовательские строки, не реализуя повторно все их возможности.
Как это делается
В этом примере мы реализуем два разных пользовательских строковых класса:
lc_string
и ci_string
. Первый создает на основе любых входных данных строки в нижнем регистре. Второй строки не преобразует, но может выполнить сравнение строк независимо от регистра.
1. Включим несколько необходимых заголовочных файлов, а затем объявим об использовании пространства имен
std
по умолчанию:
#include
#include
#include
using namespace std;
2. Затем снова реализуем функцию
std::tolower
, которая уже определена в
. Уже существующая функция работает хорошо, но она не имеет модификатора constexpr
. Однако отдельные строковые функции имеют этот модификатор, начиная с C++17, и мы хотим иметь возможность использовать их для нашего собственного строкового класса-типажа. Функция соотносит символы в верхнем регистре с символами в нижнем регистре и оставляет другие символы неизменными:
static constexpr char tolow(char c) {
switch (c) {
case 'A'...'Z': return c - 'A' + 'a';
default: return c;
}
}
3. Класс
std::basic_string
принимает три шаблонных параметра: тип основного символа, класс-типаж символа и тип allocator
. В этом разделе мы изменяем только класс-типаж символа, поскольку он определяет поведение строк. Для повторной реализации только того, что должно отличаться от типичного поведения строк, мы явно наследуем от стандартного класса-типажа:
class lc_traits : public char_traits {
public:
4. Наш класс принимает входные строки, но преобразует их в строки в нижнем регистре. Существует функция, которая делает это посимвольно, так что можно поместить здесь нашу функцию
tolow
. Она имеет модификатор constexpr
, именно поэтому мы и реализовали самостоятельно функцию tolow
с таким же модификатором constexpr
:
static constexpr
void assign(char_type& r, const char_type& a ) {
r = tolow(a);
}
5. Еще одна функция обрабатывает копирование целой строки в отдельный участок памяти. Мы используем вызов
std::transform
, чтобы скопировать все символы из исходной строки в строку — место назначения, и в то же время соотносим каждый символ с его версией в нижнем регистре:
static char_type* copy(char_type* dest,
const char_type* src,
size_t count) {
transform(src, src + count, dest, tolow);
return dest;
}
};
6. Еще один типаж помогает создать строковый класс, эффективно преобразующий строки в нижний регистр. Напишем еще один типаж, который не изменяет полученную строку, но позволяет выполнять сравнение строк независимо от регистра. Снова унаследуем от существующего стандартного класса-типажа для символов и в этот раз переопределим некоторые члены функции:
class ci_traits : public char_traits {
public:
7. Функция
eq
указывает, равны ли два символа. Реализуем такую же функцию, но будем сравнивать версии символов в нижнем регистре. При использовании этой функции символ 'A'
равен символу 'a'
.
static constexpr bool eq(char_type a, char_type b) {
return tolow(a) == tolow(b);
}
8. Функция
lt
указывает, меньше ли значение a
значения b
. Применим корректный логический оператор, но только после того, как оба символа будут преобразованы к нижнему регистру:
static constexpr bool lt(char_type a, char_type b) {
return tolow(a) < tolow(b);
}
9. Последние две функции работали для посимвольного ввода, а следующие две будут работать для строк. Функция compare работает по аналогии со старой функцией
strncmp
. Она возвращает значение 0
при условии, что обе строки равны от начала до позиции, указанной в переменной count
. Если они отличаются, то функция возвращает отрицательное или положительное число, которое указывает, какая из строк была меньше с точки зрения лексикографии. Определение разности между обоими символами в каждой позиции должно, конечно же, происходить для символов в нижнем регистре. Положительный момент заключается в том, что весь код цикла является частью функции с модификатором constexpr
, начиная с C++14.
static constexpr int compare(const char_type* s1,
const char_type* s2,
size_t count) {
for (; count; ++s1, ++s2, --count) {
const char_type diff (tolow(*s1) - tolow(*s2));
if (diff < 0) { return -1; }
else if (diff > 0) { return +1; }
}
return 0;
}
10. Последняя функция, которую нужно реализовать для нашего строкового класса, не зависящего от регистра, — это функция
find
. Для заданной входной строки p
и ее длины count
она определяет позицию символа ch
. Затем возвращает указатель на первое включение данного символа или nullptr
, если такого символа нет. Сравнение в этой функции должно выполняться с использованием функции tolow
, чтобы поиск не зависел от регистра. К сожалению, мы не можем применить функцию std::find_if
, поскольку она не имеет модификатора constexpr
, нужно писать цикл самостоятельно.
static constexpr
const char_type* find(const char_type* p,
size_t count,
const char_type& ch) {
const char_type find_c {tolow(ch)};
for (; count != 0; --count, ++p) {
if (find_c == tolow(*p)) { return p; }
}
return nullptr;
}
};
11. О’кей, с типажами мы закончили. Теперь можно определить два новых строковых типа.
lc_string
означает lower-case string (строка в нижнем регистре). ci_string
расшифровывается как case-insensitive string (строка, не зависящая от регистра). Оба класса отличаются от класса std::string
своими классами-типажами для символов:
using lc_string = basic_string;
using ci_string = basic_string