skip to content
Mehdi Mehdikhani
Table of Contents

std::tuple lets you group different types together into a single object. It’s perfect for returning multiple values from functions or storing heterogeneous data.

What std::tuple Actually Is

A tuple is like a struct, but with numbered elements instead of named ones:

#include <tuple>
// Instead of creating a struct:
struct PersonInfo {
std::string name;
int age;
double salary;
};
// You can use a tuple:
std::tuple<std::string, int, double> person_info{"John", 30, 75000.0};

Access elements with std::get<index>() or structured bindings (C++17).

Creating and Accessing Tuples

There are several ways to work with tuples:

// Creation
auto data = std::make_tuple("Alice", 25, true);
std::tuple<int, double, char> numbers{42, 3.14, 'x'};
// Access by index
std::string name = std::get<0>(data); // "Alice"
int age = std::get<1>(data); // 25
bool active = std::get<2>(data); // true
// Access by type (if unique)
std::string name2 = std::get<std::string>(data); // Same as get<0>
// C++17 structured bindings - much cleaner!
auto [person_name, person_age, is_active] = data;

When I Actually Use Tuples

Most of the time, I use tuples for:

  1. Multiple return values: Functions that need to return several things:
std::tuple<bool, std::string, int> parse_response(const std::string& response) {
if (response.empty()) {
return {false, "Empty response", 0};
}
// Parse the response...
bool success = true;
std::string message = "OK";
int code = 200;
return {success, message, code}; // Implicit tuple creation
}
// Usage with structured bindings
auto [success, message, code] = parse_response(response_data);
if (!success) {
std::cout << "Error " << code << ": " << message << std::endl;
}
  1. Temporary data grouping: When you need to store different types together briefly:
std::vector<std::tuple<int, std::string, double>> employee_data = {
{101, "John Doe", 75000.0},
{102, "Jane Smith", 82000.0},
{103, "Bob Johnson", 68000.0}
};
// Process the data
for (const auto& [id, name, salary] : employee_data) {
if (salary > 70000.0) {
std::cout << name << " (ID: " << id << ") earns $" << salary << std::endl;
}
}
  1. Sorting by multiple criteria: Tuples have built-in comparison operators:
struct Task {
int priority;
std::string name;
std::chrono::time_point<std::chrono::system_clock> deadline;
};
std::vector<Task> tasks = /* ... */;
// Sort by priority first, then by deadline
std::sort(tasks.begin(), tasks.end(), [](const Task& a, const Task& b) {
return std::make_tuple(a.priority, a.deadline) <
std::make_tuple(b.priority, b.deadline);
});
  1. Map keys with multiple values: When you need composite keys:
// Map from (year, month) to sales data
std::map<std::tuple<int, int>, double> monthly_sales;
monthly_sales[{2022, 1}] = 150000.0; // January 2022
monthly_sales[{2022, 2}] = 175000.0; // February 2022
// Find sales for a specific month
auto key = std::make_tuple(2022, 1);
if (auto it = monthly_sales.find(key); it != monthly_sales.end()) {
std::cout << "January 2022 sales: $" << it->second << std::endl;
}

Tuple Operations

Tuples support various operations:

auto tuple1 = std::make_tuple(1, "hello", 3.14);
auto tuple2 = std::make_tuple(2, "world", 2.71);
// Comparison (lexicographic)
if (tuple1 < tuple2) {
std::cout << "tuple1 is less than tuple2" << std::endl;
}
// Concatenation
auto combined = std::tuple_cat(tuple1, tuple2);
// combined is std::tuple<int, string, double, int, string, double>
// Size
constexpr size_t size = std::tuple_size_v<decltype(tuple1)>; // 3
// Type of element at index
using second_type = std::tuple_element_t<1, decltype(tuple1)>; // std::string

Real World Example

Here’s how I use tuples in a configuration parser:

class ConfigParser {
public:
using ConfigValue = std::tuple<std::string, std::string, std::optional<std::string>>;
// key, value, comment
std::vector<ConfigValue> parse_file(const std::string& filename) {
std::vector<ConfigValue> config;
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
if (auto parsed = parse_line(line); std::get<0>(parsed) != "") {
config.push_back(parsed);
}
}
return config;
}
private:
ConfigValue parse_line(const std::string& line) {
// Simple parser - key=value # comment
auto comment_pos = line.find('#');
std::optional<std::string> comment;
std::string working_line = line;
if (comment_pos != std::string::npos) {
comment = line.substr(comment_pos + 1);
working_line = line.substr(0, comment_pos);
}
auto eq_pos = working_line.find('=');
if (eq_pos == std::string::npos) {
return {"", "", comment};
}
std::string key = working_line.substr(0, eq_pos);
std::string value = working_line.substr(eq_pos + 1);
return {trim(key), trim(value), comment};
}
};
// Usage
ConfigParser parser;
auto config = parser.parse_file("app.conf");
for (const auto& [key, value, comment] : config) {
std::cout << key << " = " << value;
if (comment.has_value()) {
std::cout << " # " << comment.value();
}
std::cout << std::endl;
}

std::pair vs std::tuple

std::pair is basically a 2-element tuple with named accessors:

// pair - two elements with names
std::pair<std::string, int> name_age{"John", 30};
std::cout << name_age.first << " is " << name_age.second << " years old" << std::endl;
// tuple - can have any number of elements, access by index
std::tuple<std::string, int, bool> name_age_active{"John", 30, true};
std::cout << std::get<0>(name_age_active) << " is " << std::get<1>(name_age_active) << std::endl;
// C++17 structured bindings work with both
auto [name1, age1] = name_age;
auto [name2, age2, active] = name_age_active;

The Apply Pattern

std::apply lets you call functions with tuple elements as arguments:

void print_person(const std::string& name, int age, double salary) {
std::cout << name << " (" << age << " years old) earns $" << salary << std::endl;
}
auto person_data = std::make_tuple("Alice", 28, 95000.0);
std::apply(print_person, person_data); // Calls print_person("Alice", 28, 95000.0)
// Useful for function parameter unpacking
auto args = std::make_tuple(10, 5);
int result = std::apply([](int a, int b) { return a + b; }, args); // 15

The Pattern I Follow

I use tuples when:

  • I need to return multiple values from a function
  • I want to group heterogeneous data temporarily
  • I need composite keys for maps
  • The data doesn’t warrant defining a full struct

I avoid tuples when:

  • The grouped data represents a clear concept (use a struct instead)
  • I’m accessing elements frequently (named members are clearer)
  • The tuple gets very large (more than 4-5 elements)
// Good for tuples
auto get_min_max(const std::vector<int>& data) -> std::tuple<int, int>;
std::map<std::tuple<int, int>, std::string> coordinate_names;
// Better as structs
struct Point { double x, y, z; }; // Instead of std::tuple<double, double, double>
struct Person { std::string name; int age; }; // Instead of std::tuple<string, int>

Tuples are great for quick data grouping, but don’t overuse them - sometimes a proper struct with named fields is more readable.