Using std::pair
Master the use of std::pair
with this comprehensive guide, encompassing everything from simple pair manipulation to template-based applications
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 be0
or1
- 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 thefirst
objectsecond_type
returns the type of thesecond
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
andsecond
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 asstd::tuple_element
andstd::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.
Hash Maps using std::unordered_map
Creating hash maps using the standard library's std::unordered_map
container