Благодаря параметризованным классам на C++ пишется код для работы с различными типами данных, сам код при этом не дублируется. Рассмотрим на практических примерах, как эти классы создаются и эффективно используются.

Создание параметризованного класса

Вот простой параметризованный класс Container:

template<typename T>
class Container {
private:
T data_;

public:
Container(const T& value) : data_(value) {}

T get_data() const { return data_; }
void set_data(const T& value) { data_ = value; }
};

// Использование
int main() {
Container<int> int_container(42);
Container<std::string> string_container("Hello");

std::cout << int_container.get_data() << '\n'; // Выводится: «42»
std::cout << string_container.get_data() << '\n'; // Выводится: «Hello»
}

Реальный пример: смарт-кэш

Вот практическая реализация параметризованного Cache:

#include <unordered_map>
#include <chrono>

template<typename KeyType, typename ValueType>
class Cache {
private:
struct CacheEntry {
ValueType value;
std::chrono::steady_clock::time_point expiry;

CacheEntry(const ValueType& v, std::chrono::seconds ttl)
: value(v),
expiry(std::chrono::steady_clock::now() + ttl) {}
};

std::unordered_map<KeyType, CacheEntry> data_;
std::chrono::seconds default_ttl_;

public:
explicit Cache(std::chrono::seconds ttl = std::chrono::seconds(60))
: default_ttl_(ttl) {}

void put(const KeyType& key, const ValueType& value,
std::chrono::seconds ttl = std::chrono::seconds(0)) {
auto actual_ttl = (ttl.count() > 0) ? ttl : default_ttl_;
data_[key] = CacheEntry(value, actual_ttl);
}

bool get(const KeyType& key, ValueType& value) {
auto it = data_.find(key);
if (it == data_.end()) {
return false;
}

if (std::chrono::steady_clock::now() > it->second.expiry) {
data_.erase(it);
return false;
}

value = it->second.value;
return true;
}

void cleanup() {
auto now = std::chrono::steady_clock::now();
for (auto it = data_.begin(); it != data_.end();) {
if (now > it->second.expiry) {
it = data_.erase(it);
} else {
++it;
}
}
}
};

// Пример использования
void demonstrate_cache() {
Cache<std::string, int> number_cache;

number_cache.put("age", 25);
number_cache.put("count", 100, std::chrono::seconds(30));

int value;
if (number_cache.get("age", value)) {
std::cout << "Age: " << value << '\n';
}

// Просроченные записи удаляются
number_cache.cleanup();
}

Множественные параметры шаблона

Вот как в параметризованном классе применяются множественные параметры-типы:

template<typename KeyType, typename ValueType, typename CompareType = std::less<KeyType>>
class OrderedPair {
private:
KeyType key_;
ValueType value_;
CompareType compare_;

public:
OrderedPair(const KeyType& key, const ValueType& value)
: key_(key), value_(value) {}

bool operator<(const OrderedPair& other) const {
return compare_(key_, other.key_);
}

const KeyType& key() const { return key_; }
const ValueType& value() const { return value_; }
};

// Пример применения с пользовательским компаратором
struct CaseInsensitiveCompare {
bool operator()(const std::string& a, const std::string& b) const {
return std::lexicographical_compare(
a.begin(), a.end(),
b.begin(), b.end(),
[](char x, char y) {
return std::tolower(x) < std::tolower(y);
}
);
}
};

void demonstrate_ordered_pairs() {
// Применение компаратора по умолчанию
OrderedPair<int, std::string> pair1(1, "one");
OrderedPair<int, std::string> pair2(2, "two");

// Применение пользовательского компаратора
OrderedPair<std::string, int, CaseInsensitiveCompare>
pair3("Hello", 1),
pair4("hello", 2);
}

Специализация шаблона

Иногда для конкретных типов требуется особое поведение:

template<typename T>
class DataHandler {
public:
static std::string serialize(const T& data) {
// Параметризованная сериализация
std::ostringstream oss;
oss << data;
return oss.str();
}
};

// Специализация для «bool»
template<>
class DataHandler<bool> {
public:
static std::string serialize(const bool& data) {
return data ? "true" : "false";
}
};

// Специализация для «std::vector»
template<typename T>
class DataHandler<std::vector<T>> {
public:
static std::string serialize(const std::vector<T>& data) {
std::ostringstream oss;
oss << "[";
for (size_t i = 0; i < data.size(); ++i) {
if (i > 0) oss << ", ";
oss << DataHandler<T>::serialize(data[i]);
}
oss << "]";
return oss.str();
}
};

Реальное применение: параметризованный тип Result

Вот практическая реализация типа Result, которым обрабатываются любые значение и ошибка:

template<typename T, typename E = std::string>
class Result {
private:
std::variant<T, E> data_;
bool is_success_;

public:
Result(const T& value)
: data_(value), is_success_(true) {}

Result(const E& error)
: data_(error), is_success_(false) {}

bool is_success() const { return is_success_; }
bool is_error() const { return !is_success_; }

const T& value() const {
if (!is_success_) {
throw std::runtime_error("Attempting to get value from error result");
}
return std::get<T>(data_);
}

const E& error() const {
if (is_success_) {
throw std::runtime_error("Attempting to get error from successful result");
}
return std::get<E>(data_);
}

template<typename Func>
auto map(Func&& f) const {
using ReturnType = std::invoke_result_t<Func, T>;
if (is_success_) {
return Result<ReturnType, E>(f(value()));
}
return Result<ReturnType, E>(error());
}
};

// Пример применения
Result<int> divide(int a, int b) {
if (b == 0) {
return Result<int>("Division by zero");
}
return Result<int>(a / b);
}

void process_result() {
auto result = divide(10, 2)
.map([](int x) { return x * 2; })
.map([](int x) { return std::to_string(x); });

if (result.is_success()) {
std::cout << "Result: " << result.value() << '\n';
} else {
std::cout << "Error: " << result.error() << '\n';
}
}

Ограничения параметризованного класса на C++20

Вот как используются концепции для ограничения параметризованных классов:

#include <concepts>

template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template<Numeric T>
class Statistics {
private:
T sum_ = 0;
T square_sum_ = 0;
size_t count_ = 0;

public:
void add(T value) {
sum_ += value;
square_sum_ += value * value;
++count_;
}

double mean() const {
return count_ > 0 ? static_cast<double>(sum_) / count_ : 0.0;
}

double variance() const {
if (count_ < 2) return 0.0;
double m = mean();
return (static_cast<double>(square_sum_) / count_) - (m * m);
}
};

// Применение
void analyze_numbers() {
Statistics<int> int_stats;
int_stats.add(1);
int_stats.add(2);
int_stats.add(3);

std::cout << "Mean: " << int_stats.mean() << '\n'
<< "Variance: " << int_stats.variance() << '\n';

// Это не скомпилируется:
// «Statistics<std::string> string_stats;» // Ошибка: строка не числовая
}

Не забывайте, что параметризованные классы полностью определяются в заголовочных файлах, ведь при использовании шаблона компилятору нужно «видеть» полное его определение. Кроме того, чтобы избежать неожиданностей при работе с параметризованными классами, всегда тестируйте эти классы с различными типами.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: Generic Classes in C++: Complete Guide

Предыдущая статьяПочему Cloudflare не использует контейнеры в инфраструктуре платформы Workers?
Следующая статьяTypeScript: от нулевого до продвинутого уровня. Часть 2