std::pair
std::pair
data structure within the C++ standard library.A very common scenario we will come across is the need to store two values together. 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 values are connected in some way.
Let's introduce a new Item
struct that we will use for these lessons:
#include <string>
using std::string;
struct Item {
string Name;
int Weight;
int Value;
};
Item HealingPotion { "Healing Potion", 2, 10 };
Imagine we were trying to create an inventory system, where each slot in our inventory can contain multiple quantities of a specific item.
So, we have two closely connected values: an Item
, and how many of those items we have in our inventory. We already know we could create a struct
for that:
struct InventoryItem {
Item* Item;
int Quantity;
}
InventoryItem Slot { &HealingPotion, 1000 };
But we can also use a more generic container. One of the most basic containers is a simple pair
, available from the std
namespace by including <utility>
:
#include <utility>
std::pair Slot { &HealingPotion, 1000 };
In the above example, because we are initializing the pair
at the same time we create it, we do not have to specify the type of each member in the pair.
We can provide the types we want (demonstrated below) but the compiler can just automatically deduce the types by the values we are providing for initialization.
In the above example, we create a pair that contains a pointer to an Item
and an int
. This form of automatic type inference is called "Class Template Argument Deduction", or CTAD.
If we do not provide the initial values, the data type we want to store cannot be inferred, so we would need to provide that information upfront.
For example, a pair
that contains an Item
pointer and int
can be declared like this:
#include <utility>
std::pair<Item*, int> Slot;
CTAD is optional - we can provide both the types and initial values if we prefer:
#include <utility>
std::pair<Item*, int> Slot { &HealingPotion, 1000 };
The use of <
and >
in these examples indicates we are using a template. We will make use of templates provided by the standard library throughout this section, and we will learn how to create our own in the intermediate course.
As a final example of creating pairs, the following shows how we can create and return one from a function:
Item HealingPotion { "Healing Potion", 2, 10 };
std::pair<Item*, int> GetInventoryItem() {
return { &HealingPotion, 10 }
}
auto InventoryItem { GetInventoryItem() };
The first and second members of the pair are available through the member access operator .
with the keys first
and second
 respectively.
std::pair Slot { &HealingPotion, 1000 };
// Reading Values
Item& Item { Slot.first };
int Quantity { Slot.second };
int ItemValue { Slot.first->Value };
int TotalValue { Slot.first->Value * Quantity };
int TotalWeight { Slot.first->Weight * Quantity };
// Writing Values
Item ManaPotion { "Mana Potion", 1, 5 };
Slot.first = &ManaPotion;
Slot.second = 10;
Like with any other type, pairs can also be stored and passed around as values, references, or pointers. In the below example, we pass the pair by value, ie, we create a copy:
using std::pair, std::cout, std::endl;
void IncrementByValue(pair<Item*, int> Slot) {
Slot.second++;
}
int main() {
pair Slot { &HealingPotion, 100 };
IncrementByValue(Slot);
// Quantity is still 100 - we passed a copy of the Slot.
cout << "Quantity: " << Slot.second << endl;
}
Quantity: 100
Like with any other type, we can create a reference to a pair using the &
 operator:
// A reference to a pair
// The pair stores a pointer to an Item and an integer
std::pair<Item*, int>& Slot;
Here, we pass a reference. Note the addition of the &
in our parameter list:
using std::pair, std::cout, std::endl;
void IncrementByReference(pair<Item*, int>& Slot) {
Slot.second++;
}
int main() {
pair Slot { &HealingPotion, 100 };
IncrementByReference(Slot);
// Quantity is now 101 - we passed a reference to the Slot.
cout << "Quantity: " << Slot.second << endl;
}
Quantity: 101
Finally, lets see an example using a pointer. Note the addition of the *
on line 3 to indicate we're expecting a pointer, the switching of the .
for the ->
operator on line 4, and the use of the address-of operator &
on line 9.
using std::pair, std::cout, std::endl;
void IncrementByPointer(pair<Item*, int>* Slot) {
Slot->second++;
}
int main() {
pair Slot { &HealingPotion, 100 };
IncrementByPointer(&Slot);
// Quantity is now 101 - we passed a pointer to the Slot.
cout << "Quantity: " << Slot.second << endl;
}
Quantity: 101
The use of a pointer to a container (our pair
) that itself contains a pointer (our Item
) may be confusing but is not fundamentally different from what we’ve seen before.
We can get access to our Item*
properties through our pair*
by using the ->
operator multiple times.
using std::pair;
int GetItemValue(pair<Item*, int>* Slot) {
return Slot->first->Value;
}
As covered in our earlier lessons on pointers, we might want to ensure that a pointer is not a nullptr
before trying to dereference it, to prevent exceptions:
using std::pair;
int GetItemValue(pair<Item*, int>* Slot) {
// Ensure the std::pair* is not a nullptr
if (!Slot) return 0;
// Ensure the Item* is not a nullptr
if (!Slot->first) return 0;
return Slot->first->Value;
}
The types of pairs can get a little verbose. As with other data structures, we can alias a pair type to a simpler, more descriptive name:
using InventorySlot = std::pair<Item*, int>*;
int GetItemValue(InventorySlot Slot) {
if (!Slot) return 0;
if (!Slot->first) return 0;
return Slot->first->Value;
}
std::pair
As with classes and structs, we can use structured bindings to access the first and second elements of pairs. This allows us to use more descriptive names in our code, rather than first
and second
:
using std::pair;
// Using structured binding with a pair value
void BindingExample1(pair<Item*, int> Slot) {
auto [item, quantity] = Slot;
}
// Using structured binding with a pair pointer
void BindingExample2(pair<Item*, int>* Slot) {
auto [item, quantity] = *Slot;
}
std::pair
Here is a more complex example, using all the techniques we covered above:
#include <string>
#include <utility>
#include <iostream>
using std::string, std::pair, std::cout, std::endl;
struct Item {
std::string Name;
int Weight;
int Value;
};
using InventorySlot = std::pair<Item*, int>*;
void LogSlotDetails(InventorySlot Slot) {
auto [item, quantity] = *Slot;
cout << "This slot has " << quantity
<< " " << item->Name << "s." << endl;
cout << "Their weight is "
<< quantity * item->Weight
<< " grams." << endl;
cout << "They are worth "
<< quantity * item->Value
<< " coins." << endl;
}
int main() {
Item HealingPotion { "Healing Potion", 200, 5 };
std::pair Slot { &HealingPotion, 10 };
LogSlotDetails(&Slot);
}
This slot has 10 Healing Potions.
Their weight is 2000 grams.
They are worth 50 coins.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.