The Rest
I think we covered enough ground to make the rest of the containers pretty self-explanatory. I'll point out a few things in each and if you want more details I suggest checking out their respective chapters in A Tour of C++ or their documentation on cppreference or cplusplus.com.
One thing I will point out is that std::string
's find()
member is the oddball, returning an index or std::string::npos
.
The other containers' find()
method returns an iterator to the element or the sentinel. Some containers don't have a find()
member function.
For those containers you can use std::find()
passing in an iterator to the beginning and end of the range in the container you want to search. We'll talk more about STL algorithms like std::find()
later.
std::vector
Stores elements contiguously in memory. The class typically just contains a pointer to the first element and size of the vector.
Accessing an invalid index using operator[]
is undefined behavior but doing the same using the member function at()
with throw an std::out_of_range
exception.
As we discussed in the last chapter, appending elements to the back of a vector is cheap since capacity grows at a rate of 2x
giving us an amortized cost of O(1)
.
Likewise, popping elements off the back or removing a range of contiguous elements that includes the last element is also cheap because the vector can just decrease the value of its size
member.
It likely won't even free the memory yet because common use cases tell us that it is very likely the user will add more element to the vector.
Removing or inserting elements in the middle or beginning of the vector, on the other hand, is quite expensive. The vector will have the move all the elements coming after the insertion/removal point forward or backwards, respectively, the same amount of spaces as elements removed or inserted.
As with std::string
, you cannot index an element that is off the back of the vector (index >= size()
) but still in the allocated area of memory (index < capacity()
)
because that memory is not necessarily initialized.
std::vector<bool>
differs greatly from std::vector
because it does not adhere to the interface exactly. For space optimization reasons, an std::vector<bool>
is basically a dynamic bitset,
with each element taking up one bit. Functions like operator[]
and at()
do not return references to elements anymore, because you cannot take the address of a bit.
Instead, they return proxy objects for manipulating the bit at the specified index.
// Most containers:
std::vector ints = {1, 2, 3};
int* data = &ints[0];
int* end = data + ints.size();
for(; data != end; ++data) {
std::cout << *data << std::endl;
} // 1 2 3
// std::vector<bool>
std::vector<bool> bools = {true, false, true};
bool* data = &bools[0]; // error
bools.size() * sizeof(bool); // not the amount of memory taken up!
std::bitset
std::bitset
is basically an array of bits. The size is a template parameter like std::array
and must be known at compile time.
std::bitset<32> nums;
nums.reset(); // set all to 0
nums.set(); // set all to 1
nums.flip(); // flips each bit
nums[0] = 1; // no bounds checking
nums.set(2, 1); // set bit at index 2
nums.size(); // 32, num bits
nums.count(); // 2, number of set bits
nums.test(2); // 1, gets bit at index specified
if (nums.all() || nums.none()) {
// if all bits set or no bits set
}
if (nums.any()) {
// if at least 1 is set
}
std::string bitString = nums.to_string();
unsigned long u32 = nums.to_ulong();
unsigned long long u64 = nums.to_ullong();
std::map
Implemented with balanced binary search trees (typically Red-Black Trees). std::map
stores key value pairs, so an iterator to an std::map
returns std::pair<K, V>
where K
is the key type and V
is the value type. A pair has first
and second
member variables for accessing the respective element.
If the key passed to operator[]
(which can be a non-integer) does not exist in the map, it is created using the default constructor along with a default constructed value. at()
on the other hand will throw an exception if the element doesn't exist.
std::set
Also implemented with balanced binary search trees. Stores unique elements.
The default implementation of std::map
and std::set
use std::less<T>
to compare the elements. Therefore, keys must be comparable by operator<
or specialize std::less<T>
.
You can also specify a different comparison functor type that overloads operator()
as the second template parameter. We'll discuss this more later.
For both std::map
and std::set
, finding an element uses operator==
to compare for equality.
std::multiset
and std::multimap
Same as their respective non-multi variants except they can hold duplicate keys.
std::unorded_set
and std::unordered_map
Same as their respective ordered variants except implemented with a hash map / hash set. Due to the hash function, an iterator will traverse the map/set in an arbitrary order while their ordered variants will traverse the keys in order from least to greatest.
The keys of each must specialize std::hash<T>
or you must change the third template argument to a functor type to perform the hashing.
Keys are compared via operator==
which can be changed by changing the 4th template argument to a custom comparator type which overloads operator()
to perform the comparison.
std::forward_list
Typically a simple linked list implementation. Performs constant time insertion/deletion from anywhere in the list. Can only iterate through it one way (forward). Linear time random access.
std::list
Typically a doubly linked list. Again performs constant time insertion / deletion at any point in the list. Iterators are bidirectional. Linear time random access.
std::deque
"Double ended queue". Performs fast insertions and deletions at both ends of lists. Typically implemented as a sequence of small chunks of contiguous allocated elements.
Constant time random access but linear insertion or removal of elements in middle of deque. Due to extra bookkeeping, slower indexed access than std::vector
and a larger size of the class.
std::stack
, std::queue
, and std::priority_queue
These are not containers themselves but adapters. They provide an interface of their respective data structure using the container specified as the second template argument. std::stack
and std::queue
use std::deque
as their default container while std::priority_queue
uses std::vector
.
The comparator for std::priority_queue
is its third template argument. The priority queue cannot change the priority of existing elements. Internally, the priority queue uses functions like std::make_heap
, std::push_heap
, std::pop_heap
, and std::sort_heap
which manage a heap given a starting and ending random access iterator.
std::initializer_list
This one isn't technically part of the containers either. The purpose is to allow you to pass an arbitrary amount of the same type arguments to a constructor or function. The initializer list can be set and iterated, and that's about it.
auto sum(std::initalizer_list<int> list) {
auto sum = 0;
for (e : list) {
sum += e;
}
return sum;
}
const auto nums = {1, 2, 3, 4}; // type deduction of {} is initializer list
sum(nums); // 10
class MyClass {
std::vector<int> nums;
public:
MyClass(std::initializer_list<int> startingNums) : nums(startingNums) {}
};