В кортежи на C++ объединяются различные типы данных, при этом формального класса или структуры не создается. Это как быстрые контейнеры, в которые упаковываются связанные значения. Изучим их эффективное использование в коде.

Создание и использование базовых кортежей

Проще всего кортеж создается из std::make_tuple:

auto person = std::make_tuple("John", 30, 1.75);
// Типы: string, int, double

Эти типы также объявляются явно:

std::tuple<std::string, int, double> person{"John", 30, 1.75};

Доступ к элементам кортежа

Значения кортежа получаются несколькими способами:

auto person = std::make_tuple("John", 30, 1.75);

// Используя «std::get» с индексом
std::string name = std::get<0>(person);
int age = std::get<1>(person);

// Используя «std::get» с типом, который не повторяется
std::string name2 = std::get<std::string>(person);
double height = std::get<double>(person);

// Используя структурированную привязку версии C++17
auto [name3, age2, height2] = person;

Работа с кортежами на практике

Возвращение многочисленных значений из функций

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

std::tuple<bool, std::string, int> parseUserData(const std::string& input) {
if (input.empty()) {
return {false, "Empty input", 0};
}

// Данные обрабатываются...
return {true, "John", 30};
}

void processUser() {
auto [success, name, age] = parseUserData("some input");
if (!success) {
// Обрабатывается ошибка
return;
}
// Используются «name» и «age»
}

Поэлементное построение кортежей

std::tuple<std::string, int, double> buildProfile() {
std::tuple<std::string, int, double> profile;

// Элементы задаются отдельно
std::get<0>(profile) = "Jane";
std::get<1>(profile) = 25;
std::get<2>(profile) = 1.68;

return profile;
}

Реальные применения

1. Обработка записей базы данных

class Database {
public:
using Record = std::tuple<int, std::string, std::string, std::chrono::system_clock::time_point>;

Record getUser(int id) {
// Смоделированный запрос к базе данных
return std::make_tuple(
id,
"john_doe",
"john@example.com",
std::chrono::system_clock::now()
);
}

void processUser(int id) {
auto [userId, username, email, lastLogin] = getUser(id);

// Форматируется время последней регистрации
auto time = std::chrono::system_clock::to_time_t(lastLogin);
std::cout << "User " << username << " (ID: " << userId << ")\n"
<< "Email: " << email << "\n"
<< "Last login: " << std::ctime(&time);
}
};

2. Операции с графами

class Graph {
public:
// Возвращается {distance, path}
std::tuple<double, std::vector<int>> shortestPath(int start, int end) {
std::vector<int> path = {start, /* ... */, end};
double distance = 42.0;
return {distance, path};
}

void navigate(int start, int end) {
auto [distance, path] = shortestPath(start, end);

std::cout << "Distance: " << distance << "\n";
std::cout << "Path: ";
for (int node : path) {
std::cout << node << " ";
}
std::cout << "\n";
}
};

3. Кэш с истечением срока

template<typename T>
class Cache {
private:
using CacheEntry = std::tuple<T, std::chrono::system_clock::time_point, int>;
std::unordered_map<std::string, CacheEntry> data;

public:
void insert(const std::string& key, const T& value, int ttlSeconds) {
auto expiry = std::chrono::system_clock::now() +
std::chrono::seconds(ttlSeconds);
data[key] = std::make_tuple(value, expiry, ttlSeconds);
}

bool get(const std::string& key, T& value) {
auto it = data.find(key);
if (it == data.end()) {
return false;
}

auto& [storedValue, expiry, ttl] = it->second;
if (std::chrono::system_clock::now() > expiry) {
data.erase(it);
return false;
}

value = storedValue;
return true;
}
};

4. Система событий

class EventSystem {
public:
using EventData = std::tuple<std::string, // Тип события
std::any, // Данные о событии
std::chrono::system_clock::time_point>; // Временнáя метка

void dispatch(const std::string& type, const std::any& data) {
auto event = std::make_tuple(
type,
data,
std::chrono::system_clock::now()
);
processEvent(event);
}

private:
void processEvent(const EventData& event) {
auto [type, data, timestamp] = event;

if (type == "UserLogin") {
auto username = std::any_cast<std::string>(data);
// Регистрация обрабатывается
}
}
};

Расширенные операции над кортежами

Конкатенация кортежей

auto personalInfo = std::make_tuple("John", 30);
auto contactInfo = std::make_tuple("john@example.com", "123-456-7890");

// Кортежи объединяются при помощи «std::tuple_cat»
auto fullProfile = std::tuple_cat(personalInfo, contactInfo);
// Результат: tuple<string, int, string, string>

Работа с размером кортежа

auto profile = std::make_tuple("John", 30, 1.75);

// Получается количество элементов
constexpr size_t size = std::tuple_size<decltype(profile)>::value;
static_assert(size == 3);

// Получается тип элемента
using FirstType = std::tuple_element<0, decltype(profile)>::type;
static_assert(std::is_same_v<FirstType, std::string>);

Пользовательские типы в кортежах

class User {
std::string name;
public:
User(std::string n) : name(std::move(n)) {}
const std::string& getName() const { return name; }
};

class Role {
int level;
public:
Role(int l) : level(l) {}
int getLevel() const { return level; }
};

// Применение пользовательских типов в кортеже
auto userInfo = std::make_tuple(
User("John"),
Role(5),
std::vector<std::string>{"admin", "user"}
);

auto [user, role, permissions] = userInfo;
std::cout << user.getName() << " has role level " << role.getLevel() << "\n";

Типичные проблемы и их решения

1. Типобезопасность с get<>

auto data = std::make_tuple(1, "hello");

// Ошибка компиляции: неверный тип
// Значение целочисленного типа = std::get<1>(data); // Ошибка: индекс «1» — строка

// Корректное применение
int value = std::get<0>(data);
std::string text = std::get<1>(data);

2. Ссылочные члены

std::string name = "John";
auto tuple1 = std::make_tuple(name); // Делается копия
auto tuple2 = std::make_tuple(std::ref(name)); // Сохраняется ссылка

name = "Jane";
// В «tuple1» по-прежнему содержится «"John"»
// «tuple2» теперь ссылается на «"Jane"»

3. Сравнение кортежей

auto profile1 = std::make_tuple("John", 30);
auto profile2 = std::make_tuple("Jane", 25);

// Кортежи сравниваются лексикографически
bool isLess = profile1 < profile2; // Сначала сравнивается строка

Кортежи  —  это гибкие инструменты для написания более чистого и сопровождаемого кода. Особенно кстати они приходятся при временном объединении данных или возвращении многочисленных значений из функций. С кортежами код выразительнее, в нем меньше временных структур.

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

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


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

Предыдущая статьяИстория успеха FastAPI: как личный проект Себастьяна Рамиреса изменил экосистему Python
Следующая статьяКак использовать бесплатный хостинг для временных серверов и прототипов