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: 40Using 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 be0or1 - 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.8When 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: 42The 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: 1Structured 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: 40Structured 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: 41We 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: 41References, 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: 2They 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_typereturns the type of thefirstobjectsecond_typereturns the type of thesecondobject
#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: boolType 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 intBelow, 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 falseConcepts 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: boolIn 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 falseUsing 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: 2Below, 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.8We 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::pairis 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
firstandsecondprovide 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
constqualifiers for added flexibility. - Type aliasing can be used to simplify verbose
std::pairtypes in code. std::pairsupports tuple interfaces such asstd::tuple_elementandstd::tuple_sizefor more versatile template programming.- Concepts and type traits can be applied to
std::pairfor 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