Using std::pair

Master the use of std::pair with this comprehensive guide, encompassing everything from simple pair manipulation to template-based applications

Ryan McCombe
Updated

A common scenario we will come across is the need to store two objects in a single container. This might be to return two values from a function, to store two elements at every index of an array, or the simple desire to establish in our code that these two objects are connected in some way.

The standard library provides a very simple template that allows us to quickly implement this: std::pair

Creating a std::pair

The std::pair template is available within the <utility> header.

After including the header, we can create a std::pair like any other object. The template accepts two parameters - the types of the first and second objects we will store.

Below, we create a std::pair that stores two int objects:

#include <utility>

int main() {
  std::pair<int, int> MyPair;
}

Each type can be different if we wish. Below, we create a std::pair that stores an int and a bool:

#include <utility>

int main() {
  std::pair<int, bool> MyPair;
}

When declaring a std::pair, we can optionally initialize its elements:

#include <utility>

int main() {
  std::pair<int, bool> MyPair{42, true};
}

When doing this, we can omit the template parameters if we wish. The compiler can use class template argument deduction (CTAD) to infer the types, based on the types we used to set the initial values:

#include <utility>

int main() {
  std::pair MyPair{42, true};
}

Using std::make_pair()

The std::make_pair() function provides an alternative way to create std::pair objects:

#include <utility>

template<typename T1, typename T2>
void HandlePair(std::pair<T1, T2>) {
  // ...
}

int main() {
  auto MyPair{std::make_pair(42, true)};

  HandlePair(MyPair);
}

This function determines the type of the pair at runtime and was introduced to reduce the amount of syntax we need to write.

However, features have subsequently been added to the language that does the same thing - most notably class template argument deduction in C++17 - so it's now quite uncommon that we would use std::make_pair(). The previous example can simply be written as:

#include <utility>

template<typename T1, typename T2>
void HandlePair(std::pair<T1, T2>) {
  // ...
}

int main() {
  auto MyPair{std::pair(42, true)};
}

Type Aliasing

std::pair types can potentially get quite verbose. Remember, we can alias a type with a using statement if we want to reduce the amount of noise in our code:

#include <utility>

namespace Engine {
struct Character {};
}

namespace Social {
struct Guild {};
}

using GuildMember =
  std::pair<Engine::Character, Social::Guild>;

int main() {
  GuildMember MyPair;
}

Type Aliases

Learn how to use type aliases and utilities to simplify working with complex types.

Accessing Members

The first and second members of a std::pair are available using the first and second member variables, respectively:

#include <utility>
#include <iostream>

int main() {
  std::pair Player{"Anna", 40};

  std::cout << "Name: " << Player.first
            << "\nLevel: " << Player.second;
}
Name: Anna
Level: 40

Using std::get()

As an alternative to using the first and second member variables, we can alternatively access elements in a pair using the more general std::get function.

  • We pass the index we want to access as a template parameter. For std::pair, this will either be 0 or 1
  • We pass the pair as a function argument

It looks like this:

#include <utility>
#include <iostream>

int main() {
  std::pair Pair(42, 9.8);

  std::cout
    << "First: " << std::get<0>(Pair)
    << "\nSecond: " << std::get<1>(Pair);
}
First: 42
Second: 9.8

When the types within our pair are of different types, we can instead get an element by type, rather than index:

#include <iostream>
#include <utility>

int main() {
  std::pair Pair(42, 9.8);

  std::cout
    << "Double: " << std::get<double>(Pair)
    << "\nInt: " << std::get<int>(Pair);
}
Double: 9.8
Int: 42

The primary reason std::get exists is to support templates. For example, if our template needs to access the first object in our collection, and we do that using the first member variable, that limits our template to just being compatible with pairs.

If we use the std::get approach instead, our template could work with any type that implements std::get, not just pairs.

For example, std::array also implements std::get, so in the following example, we write a template that is compatible with both std::pair and std::array types:

#include <utility>
#include <array>
#include <iostream>

template <typename T>
void LogFirst(T Container) {
  std::cout << "\nFirst: "
    << std::get<0>(Container);
}

int main() {
  LogFirst(std::pair{1, 9.8});
  LogFirst(std::array{1, 2, 3, 4, 5});
}
First: 1
First: 1

Structured Binding

The std::pair class implements the structured binding operator, which we'll commonly use to access (and optionally rename) both the first and second variables in a single expression:

#include <utility>
#include <iostream>

int main() {
  std::pair Player{"Anna", 40};

  auto [Name, Level]{Player};

  std::cout << "Name: " << Name
            << "\nLevel: " << Level;
}
Name: Anna
Level: 40

Structured Binding

This lesson introduces Structured Binding, a handy tool for unpacking simple data structures

Updating Members

We can access and update the first and second members in the usual ways:

#include <utility>
#include <iostream>

int main() {
  std::pair Player{"Anna", 40};

  Player.first = "Roderick";
  ++Player.second;

  std::cout << "Name: " << Player.first
            << "\nLevel: " << Player.second;
}
Name: Roderick
Level: 41

We can also replace the std::pair entirely using the = operator:

#include <utility>
#include <iostream>

int main() {
  std::pair Player{"Anna", 40};
  Player = std::pair("Roderick", 41);

  std::cout << "Name: " << Player.first
            << "\nLevel: " << Player.second;
}
Name: Roderick
Level: 41

References, Pointers, and const

Like any container, the objects within a std::pair can be references or pointers:

#include <utility>
#include <iostream>

int main() {
  int x{1};
  int y{2};

  std::pair<int&, int*> Pair{x, &y};

  std::cout << "First: " << Pair.first;
  std::cout << "\nSecond: " << *(Pair.second);
}
First: 1
Second: 2

They can also use const as required. Below, our pair type stores a const reference, and a const pointer-to-const:

#include <utility>

int main() {
  int x{1};
  int y{2};

  std::pair<const int&, const int* const> Pair{
    x, &y
  };

  // Cannot modify const reference
  Pair.first++;

  // Cannot modify pointer-to-const
  *(Pair.second)++;

  // Cannot modify const pointer
  int z{3};
  Pair.second = 3;
}

The std::pair itself can also be passed around as a reference or a pointer, with or without const:

#include <utility>

void HandlePair(const std::pair<int, int> P) {
  P = std::pair(3, 4);
}

int main() {
  std::pair<int, int> Pair{1, 2};
  HandlePair(Pair);
}
error: binary '=': no operator found which takes a left-hand operand of type 'const std::pair<int,int>'

Pair Type Traits

When creating templates that receive std::pair objects, there are some useful class members we can use:

  • first_type returns the type of the first object
  • second_type returns the type of the second object
#include <utility>
#include <iostream>

template <typename T>
void HandlePair(T Pair) {
  std::cout << "First Type: "
    << typeid(T::first_type).name();
  std::cout << "\nSecond Type: "
    << typeid(T::second_type).name();
}

int main() {
  std::pair<int, bool> Pair;
  HandlePair(Pair);
}
First Type: int
Second Type: bool

Type Traits: Compile-Time Type Analysis

Learn how to use type traits to perform compile-time type analysis, enable conditional compilation, and enforce type requirements in templates.

Below, we use the first_type member with the std::same_as concept to implement different behaviors based on the first type in our template argument. In this case, we need to prepend the typename to specify that we expect T::first_type is going to be a type:

#include <utility>
#include <iostream>

template <typename T>
void HandlePair(T Pair) {
  constexpr bool FirstIsInt{
    std::same_as<typename T::first_type, int>};

  if constexpr (FirstIsInt) {
    std::cout << "First type is int";
  }
}

int main() {
  std::pair<int, int> Pair;
  HandlePair(Pair);
}
First type is int

Below, we use this member to create a concept that is satisfied if the std::pair has a first type that is a Character, or a type derived from Character:

#include <utility>
#include <iostream>

class Character {};

template <typename T>
concept FirstIsCharacter = std::derived_from<
  typename T::first_type, Character>;

void HandlePair(FirstIsCharacter auto Pair) {
  // ...
}

int main() {
  std::pair<int, int> Pair;
  HandlePair(Pair);
}

The concept is not satisfied in this example, as int does not derive from Character:

error: main.cpp(16,3):
the associated constraints are not satisfied
the concept 'FirstIsCharacter<std::pair<int,int>>' evaluated to false
the concept 'std::derived_from<int,Character>' evaluated to false

Concepts in C++20

Learn how to use C++20 concepts to constrain template parameters, improve error messages, and enhance code readability.

Tuple Interface

A tuple is a container that can store a collection of objects that potentially have different types. We'll introduce the C++ standard library's implementation of this - std::tuple - in the next chapter.

But, a std::pair is also a tuple - it is simply a tuple with its size constrained to two objects. As such, instead of writing our templates using pair-specific APIs such as first_type and second_type, we might want to use a tuple interface instead

This would allow our templates to still work with std::pair, but potentially larger tuples too.

Using std::tuple_element<>

The std::tuple_element template accepts two template parameters - the index of the type we want to retrieve, and the type of the tuple (or pair) we're querying.

The type at that index is then available as the ::type static member. Alternatively, we can use std::tuple_element_t to access the type directly.

The following three statements are equivalent, with the first_type and second_type approaches being specific to std::pair:

template <typename T>
void HandlePair(T Tuple) {
  // First type
  T::first_type;
  std::tuple_element<0, T>::type;
  std::tuple_element_t<0, T>;

  // Second type
  T::second_type;
  std::tuple_element<1, T>::type;
  std::tuple_element_t<1, T>;
}

Below, we use this to log out the types:

#include <utility>
#include <iostream>

template <typename T>
void HandlePair(T Tuple) {
  using std::tuple_element_t;
  std::cout << "First Type: "
    << typeid(tuple_element_t<0, T>).name();
  std::cout << "\nSecond Type: "
    << typeid(tuple_element_t<1, T>).name();
}

int main() {
  std::pair<int, bool> Pair;
  HandlePair(Pair);
}
First Type: int
Second Type: bool

In this example, we use it to create a concept:

#include <utility>
#include <iostream>

class Character {};

template <typename T>
concept FirstIsCharacter = std::derived_from<
  std::tuple_element_t<0, T>, Character>;

void HandlePair(FirstIsCharacter auto Pair) {
  // ...
}

int main() {
  std::pair<int, int> Pair;
  HandlePair(Pair);
}
error: main.cpp(16,3):
the associated constraints are not satisfied
the concept 'FirstIsCharacter<std::pair<int,int>>' evaluated to false
the concept 'std::derived_from<int,Character>' evaluated to false

Using std::tuple_size<>

The std::tuple_size trait receives our type as a template parameter and returns how many objects the type contains. This is available through the ::value member, or directly by using std::tuple_size_v.

For a pair specifically, this will always be 2:

#include <utility>
#include <iostream>

template <typename T>
void HandlePair(T Tuple) {
  std::cout << "Size: "
    << std::tuple_size<T>::value;

  // Alternatively:
  std::cout << "\nSize: "
    << std::tuple_size_v<T>;
}

int main() {
  std::pair<int, bool> Pair;
  HandlePair(Pair);
}
Size: 2
Size: 2

Below, we use this to implement a concept that is only satisfied by tuples of size 2:

#include <utility>
#include <iostream>

template <typename T>
concept TupleSize2 = std::tuple_size_v<T> == 2;

void HandlePair(TupleSize2 auto P) { 
  std::cout << "First: " << std::get<0>(P);
  std::cout << "\nSecond: " << std::get<1>(P);
  // ...
}

int main() {
  HandlePair(std::pair(42, 9.8));
}
First: 42
Second: 9.8

We cover more complex tuple topics, such as iteration, in our later lesson on std::tuple.

Tuples and std::tuple

A guide to tuples and the std::tuple container, allowing us to store objects of different types.

Summary

In this lesson, we explored the fundamental std::pair container, learning how to create, access, and manipulate it in a variety of ways. We also delved into how std::pair interfaces with other C++ features like structured bindings, type traits, and the tuple interface, broadening its utility.

Key Takeaways

  • std::pair is a simple, heterogeneous container for storing two related objects.
  • Pairs can be created and initialized directly or through the std::make_pair() function, with or without specifying types explicitly.
  • Member variables first and second provide direct access to the pair's elements.
  • std::get() offers an alternative way to access pair elements by index or type.
  • Structured binding allows for the convenient unpacking of a pair's elements.
  • Pairs can contain references and pointers, and use const qualifiers for added flexibility.
  • Type aliasing can be used to simplify verbose std::pair types in code.
  • std::pair supports tuple interfaces such as std::tuple_element and std::tuple_size for more versatile template programming.
  • Concepts and type traits can be applied to std::pair for more robust and type-safe template functions.
Next Lesson
Lesson 54 of 128

Hash Maps using std::unordered_map

Creating hash maps using the standard library's std::unordered_map container

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Using std::pair with Custom Types
Can I use std::pair with my own custom types, and if so, how?
Returning Multiple Values from a Function
How can I use std::pair to return multiple values from a function?
std::pair vs Custom Struct
When should I use std::pair instead of defining my own struct or class?
Using std::pair with References
Can I store references in a std::pair, and what are the implications?
Structured Binding with std::pair
How does structured binding work with std::pair, and what are the benefits?
Template Argument Deduction with std::pair
How does template argument deduction work with std::pair, and what are the advantages?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant