Tuples and std::tuple

A guide to tuples and the std::tuple container, allowing us to store objects of different types.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated

A std::tuple is a generalization of std::pair that allows storing a collection of objects of different types. While a pair is limited to two elements, a tuple can contain any number of elements. This makes tuples a flexible way to group related values.

Tuples are heterogeneous containers, meaning each element can have a different type. This is in contrast to homogeneous containers like arrays and lists where all elements have the same type.

Tuples are commonly used to return multiple values from a function, store related attributes of an object, or pass multiple arguments to a function. They provide a convenient way to work with small, fixed-size collections of diverse values.

Creating Tuples

The std::tuple class template is available from the <tuple> header. Once included, we can then create a tuple in the usual ways. We pass the types of the objects we’ll be storing as template arguments, and their values as constructor arguments:

#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 make construction require less syntax, but is less relevant now that CTAD provides the same convenience as a native feature of the language.

Member Access

Access to objects within a std::tuple is typically done through the std::get template function. We pass the index as a template argument, and the tuple as a function 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 returns 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 argument to std::get. This retrieves the element within the tuple that has that type:

#include <iostream>
#include <tuple>

int main() {
  std::tuple MyTuple{42, 3.14f, true};

  std::cout << "Float: "
    << std::get<float>(MyTuple);  
}
Float: 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:

#include <iostream>
#include <tuple>

int main() {
  // A std::tuple<float, int, float, bool>:
  std::tuple MyTuple{9.8f, 42, 3.14f, true};

  std::cout << "Float: "
    << std::get<float>(MyTuple);  
}
error: static_assert failed: 'duplicate type T in get<T>(tuple)'

Structured Binding

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

std::tie()

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

std::ignore

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

Tuple Type Traits

The <tuple> header includes two type traits that can help us when working with std::tuple containers at compile time. We covered type traits in detail earlier in the course:

Tuple Size using std::tuple_size

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

Tuple Element Type using std::tuple_element

The std::tuple_element type trait allows us to determine the subtypes our tuple is storing. It receives two template parameters: the zero-based index of the position we’re querying within the tuple, and the tuple type.

Below, we get the type of the first element (index 0) 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

Tuple Concatenation

Tuples are not designed to be resized at run time, but we can create a new tuple by concatenating 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

Tuple Iteration

The standard library’s implementation of a tuple is quite basic, 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 in general. That will be addressed in future language versions, with a feature tentatively called expansion statements.

Until then, if we need that capability, we must do a little more work. If our project relies heavily on tuples, 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 <iostream>
#include <boost/hana.hpp>
namespace hana = boost::hana;

int main(){
  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 iterating 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,

Summary

In this lesson, we covered tuples and std::tuple, which provide a way to store and work with small collections of diverse objects. The key takeaways include:

  • std::tuple¬†is a container that stores objects of different types
  • Tuples are a generalization of pairs, allowing any number of elements
  • Tuples are heterogeneous containers, with each element potentially having a different type
  • Tuples can be created by specifying the types and values, or using¬†std::make_tuple()
  • Elements are accessed using¬†std::get<index>()¬†or¬†std::get<type>()
  • Structured bindings and¬†std::tie()¬†allow unpacking tuples into individual variables
  • std::tuple_size¬†and¬†std::tuple_element¬†are type traits for querying tuple properties
  • Tuples can be concatenated using¬†std::tuple_cat()

Was this lesson useful?

Next Lesson

Introduction to Queues and std::queue

Learn the fundamentals and applications of queues with the std::queue container.
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Standard Library Data Structures
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 124 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Introduction to Queues and std::queue

Learn the fundamentals and applications of queues with the std::queue container.
Illustration representing computer hardware
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved