skip to content
Mehdi Mehdikhani
Table of Contents

std::tie is a neat utility that creates a tuple of references, making it easy to unpack multiple values at once. It’s the pre-C++17 way to handle multiple return values.

What std::tie Actually Does

std::tie creates a tuple of lvalue references to its arguments:

#include <tuple>
int a, b, c;
std::tie(a, b, c) = std::make_tuple(1, 2, 3);
// Now a=1, b=2, c=3
// It's basically creating:
// std::tuple<int&, int&, int&>(a, b, c) = std::make_tuple(1, 2, 3);

The assignment operator of the tuple handles unpacking the right-hand side.

Unpacking Function Returns

Before structured bindings (C++17), std::tie was the way to unpack multiple return values:

std::tuple<bool, std::string, int> process_data() {
// Some processing...
return {true, "Success", 42};
}
// Unpack with std::tie
bool success;
std::string message;
int result;
std::tie(success, message, result) = process_data();
if (success) {
std::cout << message << ": " << result << std::endl;
}

When I Actually Used std::tie

Before C++17, I used std::tie for:

  1. Multiple return value handling:
std::pair<iterator, bool> insert_result(const std::string& key, int value) {
// Implementation details...
return {iterator, success};
}
// Unpack the pair
std::map<std::string, int>::iterator it;
bool inserted;
std::tie(it, inserted) = my_map.insert({"key", 42});
if (inserted) {
std::cout << "Inserted successfully" << std::endl;
} else {
std::cout << "Key already exists, value is: " << it->second << std::endl;
}
  1. Ignoring specific return values:
std::tuple<int, std::string, double> get_data() {
return {42, "hello", 3.14};
}
// Only interested in the first and third values
int number;
double value;
std::tie(number, std::ignore, value) = get_data();
// std::ignore acts as a "don't care" placeholder
  1. Swapping multiple variables:
int x = 10, y = 20, z = 30;
std::tie(x, y, z) = std::make_tuple(z, x, y); // Rotate values
// Now x=30, y=10, z=20
// Or simpler swaps
std::tie(a, b) = std::make_tuple(b, a); // Swap a and b
  1. Custom comparison operators:
struct Person {
std::string first_name;
std::string last_name;
int age;
bool operator<(const Person& other) const {
// Compare by last name, then first name, then age
return std::tie(last_name, first_name, age) <
std::tie(other.last_name, other.first_name, other.age);
}
bool operator==(const Person& other) const {
return std::tie(first_name, last_name, age) ==
std::tie(other.first_name, other.last_name, other.age);
}
};

Real World Example

Here’s how I used to handle parsing functions before structured bindings:

class Parser {
public:
enum class Status { Success, InvalidFormat, UnexpectedEnd };
std::tuple<Status, int, std::string> parse_integer(const std::string& input, size_t& pos) {
if (pos >= input.length()) {
return {Status::UnexpectedEnd, 0, "Unexpected end of input"};
}
if (!std::isdigit(input[pos])) {
return {Status::InvalidFormat, 0, "Expected digit"};
}
int value = 0;
while (pos < input.length() && std::isdigit(input[pos])) {
value = value * 10 + (input[pos] - '0');
pos++;
}
return {Status::Success, value, ""};
}
void process_numbers(const std::string& input) {
size_t pos = 0;
while (pos < input.length()) {
Status status;
int value;
std::string error_msg;
// Unpack the tuple return
std::tie(status, value, error_msg) = parse_integer(input, pos);
switch (status) {
case Status::Success:
std::cout << "Parsed: " << value << std::endl;
break;
case Status::InvalidFormat:
std::cout << "Format error: " << error_msg << std::endl;
return;
case Status::UnexpectedEnd:
return; // End of input
}
// Skip whitespace
while (pos < input.length() && std::isspace(input[pos])) {
pos++;
}
}
}
};

std::tie vs Structured Bindings

C++17 structured bindings largely replaced the need for std::tie:

// Old way with std::tie
bool success;
std::string message;
int code;
std::tie(success, message, code) = get_status();
// New way with structured bindings (C++17)
auto [success, message, code] = get_status();

Structured bindings are cleaner because you declare and initialize in one step.

Using std::tie with Existing Variables

The main advantage of std::tie over structured bindings is that it works with existing variables:

class StateMachine {
int current_state_ = 0;
std::string error_message_;
public:
void update() {
// Update existing member variables
std::tie(current_state_, error_message_) = process_next_state();
}
private:
std::tuple<int, std::string> process_next_state() {
// State processing logic...
return {new_state, error_msg};
}
};

With structured bindings, you’d need to create new variables and then assign.

The std::ignore Placeholder

std::ignore is useful when you only care about some values:

std::tuple<bool, int, std::string, double> get_complex_result() {
return {true, 42, "data", 3.14};
}
// Only want the boolean and the double
bool flag;
double value;
std::tie(flag, std::ignore, std::ignore, value) = get_complex_result();

The Pattern I Used to Follow

Before C++17, my typical pattern was:

// For new variables, create them inline with std::tie
ResultType result_var;
ErrorCode error_code;
std::string error_message;
std::tie(result_var, error_code, error_message) = risky_operation();
// For existing variables, use std::tie to update them
std::tie(this->current_value_, this->last_error_) = compute_new_state();
// For comparisons, use std::tie to avoid writing complex comparison logic
bool operator<(const ComplexType& other) const {
return std::tie(field1_, field2_, field3_) <
std::tie(other.field1_, other.field2_, other.field3_);
}

Current Usage

These days, I mostly use structured bindings instead of std::tie, but I still reach for std::tie when:

  • Working with existing variables that I want to update
  • Writing comparison operators
  • Working in pre-C++17 codebases

std::tie was essential before structured bindings, and it’s still useful for specific scenarios where you need to assign to existing variables.