std::pair
std::pair
with this comprehensive guide, encompassing everything from simple pair manipulation to template-based applicationsA 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
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;
}
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)};
}
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;
}
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
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.
std::pair
, this will either be 0
or 1
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
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
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
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>'
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
objectsecond_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
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
.
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.
std::pair
is a simple, heterogeneous container for storing two related objects.std::make_pair()
function, with or without specifying types explicitly.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.const
qualifiers for added flexibility.std::pair
types in code.std::pair
supports tuple interfaces such as std::tuple_element
and std::tuple_size
for more versatile template programming.std::pair
for more robust and type-safe template functions.std::pair
Master the use of std::pair
with this comprehensive guide, encompassing everything from simple pair manipulation to template-based applications
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.