Благодаря параметризованным классам на 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;» // Ошибка: строка не числовая
}
Не забывайте, что параметризованные классы полностью определяются в заголовочных файлах, ведь при использовании шаблона компилятору нужно «видеть» полное его определение. Кроме того, чтобы избежать неожиданностей при работе с параметризованными классами, всегда тестируйте эти классы с различными типами.
Читайте также:
- C++: полное руководство по push_back
- C++: полное руководство по перечислениям
- C++: подробное руководство по массивам
Читайте нас в Telegram, VK и Дзен
Перевод статьи ryan: Generic Classes in C++: Complete Guide





