1. Introduction
Sometimes it feels like return is a cinema ticket: one ticket — one person. But we want "to bring a friend and grab popcorn". In programming this situation arises all the time: we computed something and want to return not only the main result but also extra details — for example, whether it succeeded, what the error was, what the remainder is, which index was found, and so on.
The most straightforward way is to have the function return only the "main" value, and write the rest into parameters. This works, but often harms readability: at the call site it's hard to tell what is input, what is output, and why an argument suddenly changed.
One return, but a composite type
Put simply, we do this: instead of returning int or std::string, we return, for example, a struct with fields, or a std::pair, or a std::tuple.
Visually it can be represented like this:
flowchart TD
A["The function does the work"] --> B["Collects the result from several parts"]
B --> C["Returns one object (struct / pair / tuple)"]
C --> D["External code reads parts of the result"]
Important: we are not yet discussing "convenient unpacking" of the result into multiple variables — that will be the next lecture. Our goal here is to learn to choose the right container for the result and not make the function contract murky.
To keep examples concrete, we'll develop a small console application: expense tracking. The user types lines like:
- add 120 coffee
- add 450 groceries
- total
- exit
We won't do files, exceptions, or complex parsers yet. It's enough to understand: a line needs to be parsed, parsing may succeed or fail, and we want to return more than one value.
2. struct: a self-explanatory result
When you return multiple values, there are almost always roles: "this is the command", "this is the sum", "this is the comment", "this is success/failure". struct lets you group several variables together and give names to parts of the result. This greatly reduces the chance of swapping fields and the code becomes self-documenting.
struct for a parsing result
Let's make a function that tries to parse an "add ..." command and reports the result as three variables:
bool ok;
int amount;
std::string category;
In C++ they can be combined into a single data type. For example:
#include <string>
struct AddCommand {
bool ok;
int amount;
std::string category;
};
A nice property of struct is: you look at the fields and already understand the meaning. You don't have to remember that "the second line is the category", as can happen with pair.
Now the function:
#include <string>
AddCommand parse_add_command(const std::string& line) {
if (line.rfind("add ", 0) != 0) return {false, 0, ""};
return {true, 0, "todo"}; // placeholder for now
}
This is a stub for now (we don't yet parse numbers properly), but the model is important: the function returns one object that contains both success and data.
How it looks at the call site
When you use a struct, the call site reads clearly:
#include <iostream>
#include <string>
int main() {
std::string line = "add 120 coffee";
AddCommand cmd = parse_add_command(line);
if (!cmd.ok) {
std::cout << "Bad command\n"; // Bad command
return 0;
}
std::cout << cmd.amount << " " << cmd.category << "\n"; // 0 todo
}
Yes, the data is still fake, but look at the style: cmd.ok, cmd.amount, cmd.category — everything is explicit. That's why struct is popular in application code: it lowers the "mental tax" of reading the code.
struct as part of the contract
In your expense-tracking app it's convenient to have a separate result type because it's a business entity. Today the "add" command returns amount and category. Tomorrow you may want to add comment, and the struct will expand naturally.
But if you don't want to create a separate type and you really just need to return two values from a function, there are hacks you can use. A list of hacks follows.
3. std::pair: when there are exactly two values
std::pair<T1, T2> is a standard "two values together" type. It's very convenient when the result is exactly two items and they are so obvious that you don't want to introduce a separate struct.
But pair has a weak spot: access is via .first and .second, which are not very descriptive. They are honest (first and second), but they don't reveal meaning.
Example: split a line into command and tail
Often in a CLI you need to separate the first word from the rest of the line. This is a perfect use for pair: two parts, both strings.
#include <string>
#include <utility>
std::pair<std::string, std::string> split_first_word(const std::string& line) {
std::size_t pos = line.find(' ');
if (pos == std::string::npos) return {line, ""};
return {line.substr(0, pos), line.substr(pos + 1)};
}
Here we returned two strings: the command and the tail.
Usage:
#include <iostream>
#include <string>
#include <utility>
int main() {
auto p = split_first_word("add 120 coffee");
std::cout << p.first << "\n"; // add
std::cout << p.second << "\n"; // 120 coffee
}
This is fairly readable because "first word" and "rest" is an obvious pair. But if you had a pair<int, int> and had to remember which is "min" and which is "max", it would be worse.
When pair is better than struct
Sometimes the result type is truly ephemeral and local. For example, you split a string inside a function and use it immediately. Then creating a separate struct may feel bureaucratic: "I just wanted two values, but had to create an entity like I'm registering a company".
pair in such places is a reasonable compromise: quick, standard, without extra declarations.
4. std::tuple: when there are more than two values
std::tuple<T1, T2, T3, ...> lets you return many values at once. Technically it's powerful. Humanly — you need to be careful: access is usually via std::get<0>(t), std::get<1>(t), and very quickly you get "index magic", where mixing up meanings is easier than mixing socks after laundry.
Still, tuple can be justified when there are really many values and you don't want to declare a struct, or when types are very different and you want to quickly "glue" them into a result.
Example: divmod (quotient, remainder, success)
Let's make a function "division with remainder and a success flag". Division by zero is not allowed, so we need an ok.
#include <tuple>
std::tuple<int, int, bool> divmod(int a, int b) {
if (b == 0) return {0, 0, false};
return {a / b, a % b, true};
}
Usage without unpacking (we haven't covered unpacking yet):
#include <iostream>
#include <tuple>
int main() {
auto t = divmod(10, 3);
int q = std::get<0>(t);
int r = std::get<1>(t);
bool ok = std::get<2>(t);
std::cout << q << " " << r << " " << ok << "\n"; // 3 1 1
}
It works, but note: from code get<0> you can't tell whether it's quotient or remainder. That knowledge lives in your head, not in the code. That's the main downside of tuple.
When tuple is appropriate
If you write a small internal function and the result is used immediately nearby, tuple may be acceptable. But once the result "travels" further in the code, readability drops.
In practice many teams follow a simple rule: tuple is more of a tool for local technical glue, while for a public function contract — prefer a struct.
5. How to choose: struct vs pair vs tuple
Now let's summarize everything in one table. It doesn't replace thinking, but helps make a decision without philosophy-level debates about "the nature of order".
| Criterion | |
|
|
|---|---|---|---|
| How many values | Any | Exactly 2 | Any (usually 3+) |
| Readability at use site | High: |
Medium: |
Often low: |
| Names for result parts | Yes (fields) | No () |
No (indexes) |
| Risk of mixing up order | Low | Medium | High |
| Easy to extend the result (add a field) | Yes | Not convenient (becomes tuple/struct) | Yes, but breaks indexes and meaning |
| Footprint in code (how many declarations) | You need to declare a type | No declarations needed | No declarations needed |
| Best scenario | Public contract and business semantics | Quick pair "this + that" | Internal technical result with several parts |
The main idea: if the result has semantic parts — choose a struct. If there are exactly two obvious values — pair is fine. If there are many values and you're willing to tolerate indices — tuple, but be careful.
Returning results via out-parameters
After lectures about T& and T* you might think: "Why even use struct if I can write bool parse(..., int& out_amount, string& out_cat)?". Yes, you can. Some APIs do that.
But the downside is that at the call site the variables out_amount and out_cat must be declared beforehand, and the signature becomes longer. Also there's a risk of forgetting to check the bool and using out_* as if they already contain valid data.
Returning a struct psychologically disciplines you: you first get the "result object", then explicitly check fields. The code becomes more linear and less magical.
6. Example: finding a category in the app
Back to expense tracking. Suppose we store categories and amounts in parallel vectors (not the ideal model, but early in the course it's simpler than complicating things right away).
Let's make a function that searches for a category and reports: whether it was found, and if found — the index.
Solution using a struct
#include <string>
#include <vector>
struct FindResult {
bool found;
std::size_t index;
};
FindResult find_category(const std::vector<std::string>& cats, const std::string& name) {
for (std::size_t i = 0; i < cats.size(); ++i)
if (cats[i] == name) return {true, i};
return {false, 0};
}
Usage:
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> cats{"coffee", "groceries"};
FindResult r = find_category(cats, "coffee");
if (r.found) std::cout << r.index << "\n"; // 0
}
Yes, the index value when found == false is conditional (0). We agree that you should read index only if found == true. Later in the course we'll see stricter models, but right now the principle "multiple values in one object" is what matters.
Solution using pair
If you really want, you can do this:
#include <string>
#include <utility>
#include <vector>
std::pair<bool, std::size_t> find_category2(const std::vector<std::string>& cats,
const std::string& name) {
for (std::size_t i = 0; i < cats.size(); ++i)
if (cats[i] == name) return {true, i};
return {false, 0};
}
Usage:
#include <iostream>
#include <utility>
#include <vector>
int main() {
std::vector<std::string> cats{"coffee", "groceries"};
auto p = find_category2(cats, "tea");
std::cout << p.first << "\n"; // 0
}
This works, but is .first found or index? You remember now, but in a week you might not. For such results a struct usually wins.
7. Common mistakes when returning multiple values
Mistake #1: returning a pair/tuple when parts of the result are easy to confuse.
If you return two numbers and one is "min" and the other is "max", .first and .second don't help much. After some time you'll read your code like a puzzle. In such places a struct with fields min and max unexpectedly makes the program friendlier.
Mistake #2: changing the order of returned values "on the fly".
Today you decided that tuple returns {q, r, ok}. Two days later you changed it to {ok, q, r} because "it's more convenient internally". Internally — yes, externally — it's a contract break. For pair/tuple order is part of the API. If you constantly feel the urge to reorder, that's a sign you need a struct with names.
Mistake #3: using tuple instead of a proper data model.
tuple is not a "universal container of meaning" but just a pack of values. If you start passing around tuple<int, string, bool, double> and interpret indexes differently in three places — congratulations, you've invented mini-chaos. Better name the entity once and express meaning with fields.
Mistake #4: trying to return references to local variables inside the result.
Sometimes a beginner does something like returning tuple<const string&, bool> and puts there a reference to a local string. After the function exits the local string is destroyed and the reference becomes dangerous. At our level it's safer to stick to returning values, not references, unless you're 200% sure.
Mistake #5: forgetting about the meaning of the ok/found field and reading other fields without checking.
If your result structure is {ok, value}, then value only makes sense when ok == true. If you write code that reads value first and then checks ok, you're setting up surprises. Check first, then use — it's not nitpicking but protection from strange bugs.
GO TO FULL VERSION