Шаблоны классов C++ на примере работы со строками std::string и std::wstring
При создании классов в C++ вы, возможно, столкнетесь с ситуацией, когда для обработки похожих типов данных потребуется создать практически идентичные классы, различающиеся лишь используемыми типами. Для решения таких задач можно использовать шаблоны классов C++.
В предыдущей статье мы рассмотрели создание класса для работы со строками std::string. Сегодня мы рассмотрим создание универсального класса, который позволит нам работать как со строками std::string, так и std::wstring.
Для чего нужны строки std::wstring?
Давайте проверим, что будет, если мы используем класс, созданный в предыдущей статье с русским текстом.
Обратите внимание! Прежде чем продолжить убедитесь, что файл кодировка main.cpp установлена как UTF-8!
Добавим в начало файла строки:
setlocale(LC_ALL, "Russian");
StringUtils s;
std::string str = " ПРИВЕТ МИР ";
std::cout << "\n|" << str << "|\n";
str = s.rtrim(str);
std::cout << "\n|" << str << "|\n";
str = s.ltrim(str);
std::cout << "\n|" << str << "|\n";Запустим:
| Р?Р Р?Р'РРў Р?Р?Р |
| Р?Р Р?Р'РРў Р?Р?Р |
|Р?Р Р?Р'РРў Р?Р?Р |Вместо русского текста - крякозябры.
Это происходит потому, что для хранения символов в формате UTF-8 используется 2 байта, а std::cout не умеет работать с таким типом строк.
Добавим в конец файл строки:
std::wstring str1 = L" ПРИВЕТ МИР ";
std::wcout << "\n|" << str1 << "|\n";Запустим:
| ПРИВЕТ МИР |Теперь русские буквы корректно отображаются.
Обратите внимание! Для правильного отображения мы должны в начале файла main.cpp задать локаль UTF-8 с помощью:
#include <io.h>
#include <fcntl.h>
_setmode(_fileno(stdout), _O_U16TEXT);
_setmode(_fileno(stdin), _O_U16TEXT);
_setmode(_fileno(stderr), _O_U16TEXT);
Самый простой способ заставить уже созданный класс работать с русскими строками - создать его копию и везде поменять std::string на std::wstring.
Но, если нам потребуется доработать класс, нам придется вносить изменения в оба класса что очень неудобно! Для решения подобных проблем были созданы шаблоны классов C++.
По сути код шаблона класса представляет собой шаблон, в который компилятор без участия программиста в процессе компиляции вставляет заданные типы данных, чем существенно экономит время на разработку!
Создаем шаблон класса C++
Удалим содержимое файлов stringutils.cpp и stringutils.h.
Особенностью шаблонов класса является то, что самый простой способ его объявления и реализации - использовать для этого только заголовочный файл и сразу реализовывать все методы класса внутри его объявления.
В файл stringutils.cpp добавим строку:
#include "stringutils.h"
В принципе вы можете ограничится только одним заголовочным файлом, но для личного удобства можно оставить .cpp-файл.
В файл stringutils.h добавим наш класс:
#ifndef STRINGUTILS_H
#define STRINGUTILS_H
#include <list>
#pragma once
template <typename T> class StringUtils {
public:
typedef std::list<T> strlist;
const char *TRIMABLES = " \t\n\r";
explicit StringUtils() {};
~StringUtils() {};
T rtrim(T sourceStr) {
sourceStr.erase(sourceStr.find_last_not_of(*TRIMABLES) + 1);
return sourceStr;
}
T ltrim(T sourceStr) {
sourceStr.erase(0, sourceStr.find_first_not_of(*TRIMABLES));
return sourceStr;
}
T trim(T sourceStr) {
T result = StringUtils::ltrim(StringUtils::rtrim(sourceStr));
return result;
}
StringUtils<T>::strlist split(T sourceStr, T splitter) {
StringUtils<T>::strlist data;
T tmp;
for (auto i : sourceStr) {
if (i == splitter[0]) {
data.insert(data.end(), tmp);
tmp.clear();
} else {
tmp.append(1, i);
}
}
data.insert(data.end(), tmp);
return data;
}
private:
};
#endif
Рассмотрим его подробнее.
Перед объявлением класса-шаблона всегда нужно указывать:
template <typename T>где T - наш тип, который будет подставляться в код класса.
Мы перенесли объявления
typedef std::list<T> strlist;const char *TRIMABLES = " \t\n\r";Внутрь класса, чтобы избежать проблем с видимостью.
Так же для типа strlist мы указываем шаблонный тип t
typedef std::list<T> strlist;При компиляции эта строка будет преобразована в
typedef std::list<std::string> strlist;или
typedef std::list<std::wstring> strlist;В зависимости от указанного типа.
Подобным образом T заменяется на указанный тип в остальном коде класса-шаблона.
Чтобы работать с разными типами строк мы изменили тип:
const char *TRIMABLES = " \t\n\r";Так как мы объявили тип:
strlistВнутри класса и он использует шаблон, то для его использование его нужно указывать таким образом:
StringUtils<T>::strlistОсновные изменения коснулись метода split():
Мы изменили параметр:
splitterТеперь он использует шаблон типа T.
Таким образом мы можем использовать его с любым производным типом.
Для обходя всех символов строки мы используем итератор в цикле for:
for (auto i : sourceStr) {
}Указывая тип auto нам не нужно заботиться о подставляемом в шаблоне типе!
Для сравнения текущего символа и разделителя нам достаточно использовать:
i == splitter[0]Так как i и splitter[0] после подстановки в шаблон одного типа.
В остальном алгоритм не поменялся.
Использование шаблона класса
Для использования шаблона класса нам не придется вносить множество изменений.
Изменим код в файле main.cpp
setlocale(LC_ALL, "Russian");
StringUtils<std::string> s;
std::string str = " HELLO WORLD ";
std::cout << "\n|" << str << "|\n";
str = s.rtrim(str);
std::cout << "\n|" << str << "|\n";
str = s.ltrim(str);
std::cout << "\n|" << str << "|\n";
str = " HELLO WORLD ";
std::cout << "\n|" << str << "|\n";
str = s.trim(str);
std::cout << "\n|" << str << "|\n";
str = "HELLO MY NEW|PERFECT WORLD";
std::cout << "\n|" << str << "|\n";
StringUtils<std::string>::strlist result;
result = s.split(str, "|");
for (std::string s : result) {
std::cout << s << "=";
}
str = "HELLO MY NEW PERFECT WORLD";
std::cout << "\n|" << str << "|\n";
result = s.split(str, " ");
for (std::string s : result) {
std::cout << s << "=";
}
Запустим:
| HELLO WORLD |
| HELLO WORLD|
|HELLO WORLD|
| HELLO WORLD |
|HELLO WORLD|
|HELLO MY NEW|PERFECT WORLD|
HELLO MY NEW=PERFECT WORLD=
|HELLO MY NEW PERFECT WORLD|
HELLO=MY=NEW=PERFECT=WORLD=Для объявления типа переменной теперь мы указываем тип для шаблона в скобках <>
StringUtils<std::string> s;Так же мы указываем тип для шаблона при объявлении типа strlist:
StringUtils<std::string>::strlist result;Мы так же теперь можем использовать класс StringUtils для работы с русским языком:
StringUtils<std::wstring> s1;
std::wstring str1 = L" ПРИВЕТ МИР ";
std::wcout << "\n|" << str1.c_str() << "|\n";
str1 = s1.rtrim(str1);
std::wcout << "\n|" << str1 << "|\n";
str1 = L" ПРИВЕТ МИР ";
str1 = s1.ltrim(str1);
std::wcout << "\n|" << str1 << "|\n";
str1 = L" ПРИВЕТ МИР ";
std::wcout << "\n|" << str1 << "|\n";
str1 = s1.trim(str1);
std::wcout << "\n|" << str1 << "|\n";
str1 = L"ПРИВЕТ МОЙ НОВЫЙ|ИДЕАЛЬНЫЙ МИР";
std::wcout << "\n|" << str1 << "|\n";
StringUtils<std::wstring>::strlist result1;
result1 = s1.split(str1, L"|");
for (std::wstring s : result1) {
std::wcout << s << "=";
}
str1 = L"ПРИВЕТ МОЙ НОВЫЙ ИДЕАЛЬНЫЙ МИР";
std::wcout << "\n|" << str1 << "|\n";
result1 = s1.split(str1, L" ");
for (std::wstring s : result1) {
std::wcout << s << "=";
}Запустим:
| ПРИВЕТ МИР |
| ПРИВЕТ МИР|
|ПРИВЕТ МИР |
| ПРИВЕТ МИР |
|ПРИВЕТ МИР|
|ПРИВЕТ МОЙ НОВЫЙ|ИДЕАЛЬНЫЙ МИР|
ПРИВЕТ МОЙ НОВЫЙ=ИДЕАЛЬНЫЙ МИР=
|ПРИВЕТ МОЙ НОВЫЙ ИДЕАЛЬНЫЙ МИР|
ПРИВЕТ=МОЙ=НОВЫЙ=ИДЕАЛЬНЫЙ=МИР=Заключение
Сегодня мы рассмотрели создание шаблона класса StringUtils для работы с любыми типами строк:
Создали шаблон класса;
Рассмотрели его объявление;
Рассмотрели объявление типов с шаблоном типа T;
Рассмотрели объявление методов с шаблоном типа T;
Рассмотрели изменения в методе split() для работы с типами std::string и std::wstring;
Рассмотрели использование шаблона класса для создания экземпляра класса.
Добавить комментарий