Using std::pair

Master the use of std::pair with this comprehensive guide, encompassing everything from simple pair manipulation to template-based applications
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
Abstract art representing computer programming
Ryan McCombe
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;
}

Heterogeneous Containers

A std::pair is an example of a heterogeneous container. Heterogenous means "different types" - so a heterogenous container is one where the elements can be different types.

A pair is limited to storing only two objects, but later in the course, we’ll introduce tuples and std::tuple. Tuples can store any number of objects, each potentially having a different type.

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};
}

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;
}

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

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

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

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.

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

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.

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.

Was this lesson useful?

Next Lesson

Hash Maps using std::unordered_map

Creating hash maps using the standard library's std::unordered_map container
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Using std::pair

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

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
Hash Sets and Hash Maps
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:

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

Hash Maps using std::unordered_map

Creating hash maps using the standard library's std::unordered_map container
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved