std::tuple
std::tuple
container. Tuples allow us to store any number of objects, and those objects can have different types.We can think of a std::tuple
as a simple extension of a std::pair
. Whilst a pair can hold two objects, a tuple can hold any number of objects. And, like a pair, these objects can have different types.
In other documentation, the word heterogeneous is often used, meaning “different types”. For example, pairs and tuples are sometimes called heterogeneous containers as, unlike arrays, each object in the container can have a different type.
The std::tuple
class is available from the <tuple>
header. Once included, we can then create a tuple in the usual way. We pass the types of our objects as template parameters, and their values as constructor parameters:
#include <tuple>
std::tuple<int, float, bool> MyTuple{
42, 3.14f, true
};
When initializing the tuple with values, we can omit the template types, allowing the compiler to deduce them using class template argument deduction (CTAD)
#include <tuple>
std::tuple MyTuple{42, 3.14f, true};
The std::make_tuple
function also allows tuples to be constructed with deduced types:
#include <tuple>
void HandleTuple(
std::tuple<int, float, bool> Object){
// ...
}
int main(){
HandleTuple(std::make_tuple(42, 3.14f, true));
}
This function was introduced to remove the need to specify types, but is less relevant now that CTAD provides the same convenience as a native feature of the language.
Access to objects within a std::tuple
is typically done through the std::get
template function. We pass the index as a template parameter, and the tuple as an argument:
#include <tuple>
#include <iostream>
int main(){
std::tuple MyTuple{42, 3.14f, true};
std::cout << std::get<0>(MyTuple) << ", ";
std::cout << std::get<1>(MyTuple) << ", ";
std::cout << std::get<2>(MyTuple);
}
42, 3.14, 1
The std::get
function the element by reference, allowing it to be updated:
#include <tuple>
#include <iostream>
int main(){
std::tuple MyTuple{42, 3.14f, true};
std::cout << "First element: "
<< std::get<0>(MyTuple);
std::get<0>(MyTuple) = 5;
std::cout << "\nNew first element: "
<< std::get<0>(MyTuple);
}
First element: 42
New first element: 5
We can also pass a type as the template parameter to std::get
. This retrieves the element within the tuple that has that type:
#include <tuple>
#include <iostream>
int main(){
std::tuple MyTuple{42, 3.14f, true};
std::cout << std::get<float>(MyTuple);
}
3.14
This assumes the tuple has exactly one element of that type. If this is not the case, we will get a compilation error.
std::tie
Similar to pairs, we can unpack tuple objects to dedicated variables using structured binding:
#include <tuple>
#include <iostream>
int main(){
std::tuple MyTuple{42, 3.14, "Hello"};
auto [a, b, c]{MyTuple};
std::cout << a << ", " << b << ", " << c;
}
42, 3.14, Hello
When the variables we want to use have already been declared, we can use std::tie
to update them. Here, we reuse our a
, b
, and c
variables to store new values:
#include <tuple>
#include <iostream>
int main(){
using std::cout, std::tuple;
tuple MyTuple{42, 3.14, true};
auto [a, b, c]{MyTuple};
cout << a << ", " << b << ", " << c;
tuple AnotherTuple{100, 9.8, false};
std::tie(a, b, c) = AnotherTuple;
cout << '\n' << a << ", " << b << ", " << c;
}
42, 3.14, 1
100, 9.8, 0
When using std::tie
, we can pass std::ignore
in any position to skip over that element in the tuple. Below, we skip over the middle element, only updating a
and c
:
#include <tuple>
#include <iostream>
int main(){
using std::cout, std::tuple, std::ignore;
tuple MyTuple{42, 3.14, true};
auto [a, b, c]{MyTuple};
cout << a << ", " << b << ", " << c;
tuple AnotherTuple{100, 9.8, false};
std::tie(a, ignore, c) = AnotherTuple;
cout << '\n' << a << ", " << b << ", " << c;
}
42, 3.14, 1
100, 3.14, 0
Similar to std::array
, tuples have a fixed, static size, specified at compile time. We can get the size of a tuple by passing its type to the std::tuple_size
type trait. Usually, this is done using the shorthand std::tuple_size_v
 syntax:
#include <tuple>
#include <iostream>
template <typename T>
void HandleTuple(T Tuple){
std::cout << "Tuple size: "
<< std::tuple_size_v<T>;
}
int main(){
HandleTuple(std::tuple{42, 3.14, true});
}
Tuple size: 3
We have a dedicated lesson on type traits earlier in the course
The std::tuple_element
type trait allows us to determine the type of any object in the tuple. It receives two template parameters: the index of the element we’re querying within the tuple, and the tuple class.
Below, we get the type of the first element of our tuple using the shorthand syntax std::tuple_element_t
. We then compare it to the int
type using std::is_same_v
:
#include <tuple>
#include <iostream>
template <typename T>
void HandleTuple(T Tuple){
if constexpr (std::is_same_v<
std::tuple_element_t<0, T>, int>) {
std::cout <<
"The first element is an integer";
}
}
int main(){
HandleTuple(std::tuple{42, 3.14, true});
}
The first element is an integer
Tuples are not designed to be resized at run time, but we can concatenate two or more tuples together using the std::tuple_cat
 function:
#include <tuple>
#include <iostream>
int main(){
std::tuple Original{42, 3.14, true};
std::tuple Additions{9.8f, "Hello"};
auto Combined{
std::tuple_cat(Original, Additions)};
std::cout << "Combined Tuple Size: "
<< std::tuple_size_v<decltype(Combined)>;
std::cout << "\nLast element: "
<< std::get<4>(Combined);
}
Combined Tuple Size: 5
Last element: Hello
The standard library’s implementation of a tuple is a very basic implementation, with very limited capabilities. The type does not provide any assistance with iteration.
Additionally, the C++ language itself has limited support for iteration over heterogeneous collections like a tuple. That is planned to be addressed in future versions of the language, with a feature tentatively called expansion statements.
Until then, if we need that capability, we need to do a little more work. We could introduce a third-party library with a more robust implementation.
The following is an example using Boost.Hana in conjunction with a lambda expression. We cover lambda expressions in detail in our later chapter on functions:
#include <boost/hana.hpp>
#include <iostream>
int main(){
namespace hana = boost::hana;
hana::tuple Tuple{42, 3.14, true};
hana::for_each(Tuple, [](auto Object){
std::cout << Object << ", ";
});
}
42, 3.14, 1,
The following is an example of how to iterate over a tuple using only the standard library. It is also using more advanced functional concepts that we cover in the later chapter:
#include <tuple>
#include <iostream>
void Log(auto Object){
std::cout << Object << ", ";
}
int main(){
std::tuple MyTuple{42, 3.14f, true};
std::apply([](auto... Objects){
(Log(Objects), ...);
}, MyTuple);
}
42, 3.14, 1,
— First Published
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.