Embercode
✕
C++ Functions and ScopeLesson 7 of 17

35 min read

Returning multiple values

Why this matters

Many functions need to give back more than one answer. A function that finds the smallest and largest value in a list gives two numbers. A function that divides one number by another gives a quotient and a remainder. A function that reads a line such as Asha 88 gives a name and a score.

A C++ function can return only one value directly. Its return type is one type, such as int or std::string. To send back two or three results, a beginner often reaches for a workaround. The two common workarounds are an out parameter, which is a reference argument the function writes into, and a small struct. This lesson shows the standard library tools, std::pair and std::tuple, which bundle several values into one return value. It also shows when an out parameter is the wrong tool.

The samples use the standard library and compile with g++ -std=c++20 -Wall. The output shown was produced by running them with g++ 15.2 on Linux. Functions were covered in the earlier lessons of this module, and references were covered in the lesson on pass by value and reference, so this lesson uses both without repeating them.

One return value, and what that means in practice

A return type is written before the function name. It says what kind of value the caller gets back.

int twice(int n) {
    return n * 2;
}

The caller uses the result in an expression, such as int x = twice(4);. One call gives one value.

If you need two values, you could write two separate functions. One function returns the smallest value and another returns the largest. That works, but it scans the list twice, and the two results are not kept together. A better design returns both values from one call.

Returning a pair

std::pair is a type in the header <utility>. It holds exactly two values. The two values can have different types. You name the parts first and second.

Here is a function that returns the smallest and largest value of a list of numbers. It is example 1, and it builds on the idea of a loop over a std::vector.

std::pair<int, int> min_and_max(const std::vector<int>& values) {
    int lo = values.front();
    int hi = values.front();
    for (int v : values) {
        if (v < lo) lo = v;
        if (v > hi) hi = v;
    }
    return {lo, hi};
}

Read the parts one at a time.

  • std::pair<int, int> is the return type. It says the function returns a pair of two int values.
  • values.front() is the first element of the vector. Both lo and hi start there, so the loop has a correct starting point even when the list has one element.
  • const std::vector<int>& is a reference to the caller's list. The const promises that the function does not change it. The & means the list is not copied.
  • return {lo, hi}; builds the pair. The braces create a pair from the two values in order. The first value goes into first and the second into second.

The caller can use the pair in two ways. The first way uses .first and .second.

auto result = min_and_max(nums);
std::cout << result.first << " " << result.second << '\n';

The second way uses a feature called structured bindings, added in C++17. A structured binding unpacks the pair into named variables in one line.

auto [lo, hi] = min_and_max(nums);
std::cout << "min=" << lo << " max=" << hi << '\n';

Here auto lets the compiler work out the types, and [lo, hi] declares two new variables. lo gets the first value and hi gets the second. The names are chosen by you. They do not have to match the names used inside the function.

The full program for this lesson's examples is shown at the end of the examples section. Its output for this function is:

min=2 max=9

The input list was {7, 2, 9, 4, 2}. The smallest value is 2 and the largest is 9.

Returning a tuple

std::tuple is a type in <tuple>. It holds any number of values, and each can have its own type. A pair is a tuple with exactly two values. When you need three or more values, use a tuple.

Example 2 divides one integer by another and returns the quotient and the remainder.

std::tuple<int, int> divide_with_remainder(int a, int b) {
    return {a / b, a % b};
}

The / operator gives the whole-number part of the division. The % operator, called the remainder operator, gives what is left over. For 17 divided by 5, the quotient is 3 and the remainder is 2, because 5 times 3 is 15 and 17 minus 15 is 2.

The caller unpacks the tuple with a structured binding, the same way as a pair.

auto [q, r] = divide_with_remainder(17, 5);
std::cout << "17 / 5 = " << q << " remainder " << r << '\n';

Output from the full program:

17 / 5 = 3 remainder 2

A tuple can also be read by position with std::get. The number in angle brackets is the position, counted from 0.

auto t = divide_with_remainder(9, 4);
std::cout << "get<0>=" << std::get<0>(t) << " get<1>=" << std::get<1>(t) << '\n';

Output from the full program:

get<0>=2 get<1>=1

The position number must be a fixed number that the compiler knows when it builds the program. You cannot write std::get<i>(t) with a loop variable i, because the type of the result would change on each pass.

Ignoring a value you do not need

Sometimes a function returns three values and you need only one. std::tie lets you assign a tuple's parts to variables you already have. std::ignore is a placeholder that throws away one part.

int only_q = 0;
std::tie(only_q, std::ignore) = divide_with_remainder(20, 6);
std::cout << "only quotient=" << only_q << '\n';

Output from the full program:

only quotient=3

For 20 divided by 6, the quotient is 3. The remainder is thrown away. std::tie works with variables that already exist. A structured binding always creates new variables, so you use std::tie when you want to assign into variables declared earlier.

Example 3: a line of text with two parts

Pairs are useful for data that comes from text. Example 3 reads a line with a name and a score, separated by a space, such as Asha 88.

std::pair<std::string, int> parse_score(const std::string& line) {
    auto space = line.find(' ');
    std::string name = line.substr(0, space);
    int score = std::stoi(line.substr(space + 1));
    return {name, score};
}

The function does three jobs.

  1. line.find(' ') returns the position of the first space. Positions count from 0, so in Asha 88 the space is at position 4.
  2. line.substr(0, space) returns the text before the space. That is Asha.
  3. std::stoi turns the text after the space into an int. That is 88.

The function returns the name and the score together, so the caller does not have to split the text again.

auto [name, score] = parse_score("Asha 88");
std::cout << name << " scored " << score << '\n';

Output from the full program:

Asha scored 88

The example has one weakness that you should know about. If the line has no space, find returns std::string::npos, and both substr calls then return the whole line, so std::stoi throws. If the score is not a number, std::stoi throws an exception. Handling bad input is covered in the lessons on input validation, so this lesson does not add checks.

The full program

Each example above is part of one program. The complete file is shown here, so you can copy it and run it. The output is the output shown in the examples, in order.

#include <iostream>
#include <tuple>
#include <utility>
#include <vector>
#include <string>
 
std::pair<int, int> min_and_max(const std::vector<int>& values) {
    int lo = values.front();
    int hi = values.front();
    for (int v : values) {
        if (v < lo) lo = v;
        if (v > hi) hi = v;
    }
    return {lo, hi};
}
 
std::tuple<int, int> divide_with_remainder(int a, int b) {
    return {a / b, a % b};
}
 
std::pair<std::string, int> parse_score(const std::string& line) {
    auto space = line.find(' ');
    std::string name = line.substr(0, space);
    int score = std::stoi(line.substr(space + 1));
    return {name, score};
}
 
void divide_out(int a, int b, int& quotient, int& remainder) {
    quotient = a / b;
    remainder = a % b;
}
 
int main() {
    std::vector<int> nums{7, 2, 9, 4, 2};
    auto [lo, hi] = min_and_max(nums);
    std::cout << "min=" << lo << " max=" << hi << '\n';
 
    auto [q, r] = divide_with_remainder(17, 5);
    std::cout << "17 / 5 = " << q << " remainder " << r << '\n';
 
    auto [name, score] = parse_score("Asha 88");
    std::cout << name << " scored " << score << '\n';
 
    int a = 0, b = 0;
    std::tie(a, b) = std::pair{3, 4};
    std::cout << "a=" << a << " b=" << b << '\n';
 
    int only_q = 0;
    std::tie(only_q, std::ignore) = divide_with_remainder(20, 6);
    std::cout << "only quotient=" << only_q << '\n';
 
    int quo = 0, rem = 0;
    divide_out(17, 5, quo, rem);
    std::cout << "out params: " << quo << " " << rem << '\n';
 
    auto t = divide_with_remainder(9, 4);
    std::cout << "get<0>=" << std::get<0>(t) << " get<1>=" << std::get<1>(t) << '\n';
 
    auto p = min_and_max({5});
    std::cout << "single value: " << p.first << " " << p.second << '\n';
    return 0;
}

Output from the full program, with g++ -std=c++20 -Wall:

min=2 max=9
17 / 5 = 3 remainder 2
Asha scored 88
a=3 b=4
only quotient=3
out params: 3 2
get<0>=2 get<1>=1
single value: 5 5

The last line calls min_and_max with a list of one number, {5}. The function still works, because both lo and hi start at the only element.

The program also contains divide_out, which uses an out parameter. The next section explains when that style is the wrong choice.

Pair or tuple

Use a pair when the function returns exactly two values. The names first and second are then enough for the reader to follow, and the type std::pair<int, int> is short. Use a tuple when there are three or more values. A pair also has .first and .second as fields, so code that reads the parts by name works without a structured binding.

Both types return their parts by value. The function builds the pair or tuple, and the caller receives a copy. For numbers and short strings, the copy is cheap, so the choice between the two types should rest on how many values there are, not on speed.

When a named struct is clearer

A pair or tuple has no field names. first and second say only which position a value is in. When the two values have a clear meaning, a small struct with named fields is easier to read. A struct is a type you define yourself. It groups named values into one type.

struct Split {
    int quotient;
    int remainder;
};
 
Split divide_named(int a, int b) {
    return {a / b, a % b};
}

The caller reads the fields by name.

Split s = divide_named(17, 5);
std::cout << s.quotient << " r " << s.remainder << '\n';

Output:

3 r 2

The struct removes the risk of swapped names, because s.quotient cannot be confused with s.remainder by position. The cost is one more definition in the code. For a value used in one place, a pair or tuple is enough. For a result used across several functions, or with more than three parts, a named struct is usually the better choice.

When an out parameter is the wrong choice

An out parameter is a reference parameter that the function writes into. Here is divide_out from the full program.

void divide_out(int a, int b, int& quotient, int& remainder) {
    quotient = a / b;
    remainder = a % b;
}

The caller must create two variables first, then pass them in.

int quo = 0, rem = 0;
divide_out(17, 5, quo, rem);

This works, and it is still used in some code. For the division job, a tuple return is usually clearer, for four reasons.

  • The return value says what the function gives back. With divide_out, the caller must read the parameter list to learn which argument receives the quotient. Both out arguments have the same type, so the compiler does not catch a swapped pair. divide_out(17, 5, rem, quo) compiles and gives the wrong answer.
  • The caller must declare the variables before the call. A tuple return lets the caller write auto [q, r] = divide_with_remainder(17, 5); in one line, with no separate declarations.
  • An out parameter must be a variable that can be changed. You cannot pass a literal or a temporary value. A compile error is the result, as shown in the mistakes section below.
  • A function with out parameters cannot be used inside a bigger expression, because it returns nothing. A function that returns a pair can be passed straight into another call.

Out parameters are not always wrong. Some standard library functions use them on purpose. std::getline(std::cin, line) reads text into line, a string the caller already owns. The caller keeps the same string, which can be reused for many lines, and no new string is created on each call. The function also returns the input stream, so a loop can test whether reading worked. That case has a real reason to write into an existing variable.

A simple rule works for most beginner code. If the function computes new results, return them in a pair or tuple. If the caller already owns a large object and the function fills it in, an out parameter or a reference to an existing object is reasonable.

The table below compares the two styles for the division example.

StyleCall siteRisk
Tuple returnauto [q, r] = divide_with_remainder(17, 5);Names are chosen by the caller, so a mismatch in names is possible
Out parametersdivide_out(17, 5, quo, rem);A swapped pair of arguments compiles without a warning

The risk in the tuple column is real. Example: auto [r, q] = divide_with_remainder(17, 5); compiles, and r then holds the quotient. The compiler cannot see your intent. Choose names that match the order in the function's comment or documentation.

Common mistakes

Too many names in a structured binding

A structured binding must name every part of the pair or tuple. If you name more parts than the value has, the compiler stops with an error.

auto [q, r, extra] = divide_with_remainder(17, 5);

The compiler reports this error:

error: 3 names provided for structured binding

The tuple has two parts, so the binding needs two names. Use std::ignore with std::tie if you want to skip a part, or count the parts again.

Passing a literal to an out parameter

An out parameter must refer to a variable. A literal such as 0 is not a variable, so the call fails.

divide_out(17, 5, 0, 0);

The compiler reports:

error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'

The message says that int& expects a variable that can be changed, and the literal 0 is a temporary value. Declare int quo = 0; first and pass the variable.

Creating a pair with too many values

A pair holds exactly two values. Giving it three is an error.

std::pair<int, int> p{1, 2, 3};

The compiler reports:

error: no matching function for call to 'std::pair<int, int>::pair(<brace-enclosed initializer list>)'

If you need three values, use std::tuple.

Getting a tuple position that does not exist

Positions in std::get start at 0 and must be within the tuple's size. A tuple of three values has positions 0, 1 and 2.

auto t = std::make_tuple(1, "two", 3.0);
std::cout << std::get<3>(t) << '\n';

The first error line from g++ 15.2 is:

error: static assertion failed: tuple index must be in range

The message comes from a file inside the standard library, so the lines after it are long. Look at the first line and at the line number in your own file to find the bad index.

Ignoring a value that the function marks as important

Some functions use the [[nodiscard]] attribute to say that the caller must use the result. When you call the function and throw the result away, g++ warns you.

[[nodiscard]] int add(int a, int b) { return a + b; }
int main() {
    add(2, 3);
}

Compiling with -Wall gives this warning:

warning: ignoring return value of 'int add(int, int)', declared with attribute 'nodiscard' [-Wunused-result]

The attribute is optional. The warning shows that the call did no visible work, which is usually a sign of a bug.

Practice

Each exercise has its answer below it. Try the exercise before you read the answer.

Exercise 1

Write a function first_and_last that takes a const std::vector<int>& and returns a std::pair<int, int>. The first value is the first element, and the second value is the last element. Call it on the list {4, 9, 1} and print both values.

Show the answer

std::pair<int, int> first_and_last(const std::vector<int>& v) {
    return {v.front(), v.back()};
}
 
int main() {
    std::vector<int> v{4, 9, 1};
    auto [f, l] = first_and_last(v);
    std::cout << f << " " << l << '\n';
}

Output:

4 1

Exercise 2

Write a function to_hms that takes a number of seconds as an int and returns a std::tuple<int, int, int> with hours, minutes and seconds. For example, 3725 seconds is 1 hour, 2 minutes and 5 seconds. Print the result as h:m:s.

Show the answer

std::tuple<int, int, int> to_hms(int total) {
    return {total / 3600, (total % 3600) / 60, total % 60};
}
 
int main() {
    auto [h, m, s] = to_hms(3725);
    std::cout << h << ":" << m << ":" << s << '\n';
}

Output:

1:2:5

Exercise 3

The function below returns the total and the count. A caller wrote the code after it. Find the bug, and say why the compiler does not report it.

std::pair<int, int> sum_and_count(const std::vector<int>& v) {
    int total = 0;
    for (int x : v) total += x;
    return {total, static_cast<int>(v.size())};
}
 
auto [count, total] = sum_and_count({4, 9, 1});

Show the answer

The names are in the wrong order. The first value is the total, but the caller calls it count. The compiler accepts the code, because both parts are int and the binding names are the caller's choice. The fix is to match the order:

auto [total, count] = sum_and_count({4, 9, 1});

With the fix, total is 14 and count is 3.

Exercise 4

Rewrite this out-parameter function so that it returns a std::pair<int, int> instead. Then write the new call.

void get_min_max(const std::vector<int>& v, int& lo, int& hi) {
    lo = *std::min_element(v.begin(), v.end());
    hi = *std::max_element(v.begin(), v.end());
}

Show the answer

std::pair<int, int> get_min_max(const std::vector<int>& v) {
    auto [lo, hi] = std::minmax_element(v.begin(), v.end());
    return {*lo, *hi};
}
 
auto [lo, hi] = get_min_max({4, 9, 1});

The new version needs no variables declared before the call. It also cannot be called with its two results in the wrong order without the names showing it.

Exercise 5

A function min_max_sum returns a std::tuple<int, int, int> with the smallest value, the largest value and the sum. Write code that reads only the largest value into an existing int variable named hi, using std::tie and std::ignore.

Show the answer

int hi = 0;
std::tie(std::ignore, hi, std::ignore) = min_max_sum(v);

The largest value is in the middle position, which is position 1 when counting from 0. The other two parts are ignored.

Summary

  • A function returns one value directly, so use std::pair for two values and std::tuple for three or more.
  • Read a returned pair or tuple with a structured binding, such as auto [lo, hi] = f();, or with std::tie when the variables already exist.
  • The caller chooses the names in a structured binding, so check the order of the returned values.
  • An out parameter is a reference the function writes into. It is a reasonable choice when the caller already owns the object, as with std::getline.
  • For new results, a returned pair or tuple is usually clearer than out parameters, because a swapped argument pair compiles without an error.
  • When the parts have clear names, a small struct with named fields is easier to read than a pair.
Check yourself: Returns (6 questions) →Practice this topic: coding problems for C++ Functions and Scope →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Module assignment: Number helper