Vectors
Vectors allow us to create dynamically-sized collections of ordered data of a uniform type.
đź’ˇ Best Practice:
With Unreal Engine use the TArray
container class in place of arrays and vectors.
Table of Contents
- Defining and Initializing Vectors
- Get and Set Vector Elements
- .at() and Out of Bounds Checking
- Vector Length
- Resizing Vectors
- Clearing Out a Vector
- Growing Vectors One Element at a Time
- Vector as Stack
- Looping Over Vectors
- Passing Vectors to Functions
- Vector Capacity
- Comparing Vectors
- Copying Vectors
- Pointers and Iterators
- Further Reading
Defining and Initializing Vectors
Standard vectors can be used by including the <vector>
header.
#include <vector>
Part of the std
namespace, they are defined with a type and an optional length or list of elements.
std::vector<double> myVector; // vector of floats with length 0.
std::vector<float> iceCreamFloats(3); // vector of floats of length 3.
std::vector<int> evenNumbers{ 2, 4, 6, 8, 10, 12 }; // vector of int of length 6.
If we provide an initializer list, the type becomes optional as it can be inferred by the compiler.
std::vector evenNumbers{ 2, 4, 6, 8, 10, 12 }; // Vector of int
⏳ Wait For It:
The <>
braces used to specify the element type indicates that vectors are template classes.
Get and Set Vector Elements
Vectors elements can be retrieved and set using square braces:
std::vector temperatures{ -34.2, -30.0, -32.5, -25.3, -20.0 };
// Get using square braces:
std::cout << temperatures[3];
// Set using square braces:
temperatures[0] = -38.7;
There are also special accessor methods for the first and last element:
double first = temperatures.front();
double last = temperatures.back();
.at() and Out of Bounds Checking
Like with arrays, square brace access of vectors is not boundary checked, which can led to bugs and security holes.
The .at()
method is a boundary-safe way to get vector elements, which will throw an exception if an out of bounds index is requested:
std::vector temperatures{ -34.2, -30.0, -32.5, -25.3, -20.0 };
// Get using .at():
std::cout << temperatures.at(3);
// This will throw an exception:
std::cout << temperatures.at(333);
⏳ Wait For It:
Exceptions will be covered in more detail in a later section.
Vector Length
The length of a vector can be retrieve using the size()
method:
auto length = temperatures.size();
Resizing Vectors
A vector’s length can be modified using the resize()
method. The first argument is the new length, the second argument is the optional default value of any newly created elements.
// Assumes: #include <string> and #include <vector>
std::vector poem{ "Mares eat oats", "Goats eat oats", "Little lambs eat ivy" };
poem.resize(2); // Shrink the vector: No more little lambs.
poem.resize(10, ""); // Resize to length 10 and fill with empty strings.
If the default value isn’t provided:
- Vectors of primitives are filled with zeros.
- Vectors of objects are filled using the default constructor of the vector’s type.
Clearing Out a Vector
Instead of using .resize(0)
to truncate a vector you can use the .clear()
method. There likely isn’t a performance difference between the two, but clear()
is more intention revealing to other humans.
We can also test if an vector is empty using the .empty()
predicate method.
std::vector bagOfHolding{ "sword", "shield", "potion" };
bagOfHolding.clear();
if (bagOfHolding.empty()) {
std::cout << "You have no inventory!\n";
}
Growing Vectors One Element at a Time
We can use push_back()
to increase a vector’s length by one while adding a new element to the end of the vector:
std::vector poem{ "Mares eat oats", "Goats eat oats", "Little lambs eat ivy" };
poem.push_back("A kiddley divey too, wouldn't you?");
There’s also emplace_back()
which constructs new objects to be put at the end, rather than copying/moving as will happen with push_back()
. Learn more about emplace_back
.
Vector as Stack
We can use a vector as a LIFO (Last in First Out) stack using push_back()
to add to elements the end of the vector, back()
to retrieve elements from the end of the vector, and pop_back()
to remove elements from the end of the vector.
std::vector oddNumbers{ 1, 3, 5, 7 }; // Length is 4.
oddNumbers.push_back(9); // Length is 5.
oddNumbers.push_back(11); // Length is 6.
int eleven = oddNumbers.back();
oddNumbers.pop_back(); // Length is 5.
int nine = oddNumbers.back();
🎵 Note:
The Standard library includes actual stacks and queues: std:stack
, std:queue
, and std:deque
.
Looping Over Vectors
We can loop over vectors with standard and range-based for
loops.
This demo code shows both styles of loops and also includes the use of push_back()
and resize()
:
#include <iostream>
#include <vector>
#include <string>
int main() {
std::vector poem{ "Mares eat oats", "Goats eat oats" };
poem.push_back("Little lambs eat ivy");
for(auto i = 0; i < poem.size(); ++i) {
std::cout << "Regular For: " << poem[i] << "\n";
}
for(auto line : poem) {
std::cout << "Range For: " << line << "\n";
}
poem.resize(20, "");
for(auto line : poem) {
std::cout << "With Blanks: " << line << "\n";
}
}
Passing Vectors to Functions
Vectors can be passed to and from functions. Vector function parameters are often defined as as const &
references for performance reasons:
// Array passed as a reference for performance and as a const for safety:
void calculateAverage(const std::vector<double>& data) {
double sum{ 0.0 };
for (number : data) {
sum += data;
}
return data / data.size();
}
C++ functions are pass-by-value by default, meaning copies are made of the arguments passed to a function. We can switch to pass-by-reference with the &
operator. Passing a reference to an argument avoids the performance hit of the copy.
Vector Capacity
Because resize operations on vectors are expensive, a vector will sometimes preallocate capacity for future elements without changing its length:
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<int> oddNumbers{ 1, 3, 5, 7 }; // Length is 4. Capacity is 4.
std::cout << "Size: " << oddNumbers.size()
<< " Capacity: " << oddNumbers.capacity() << "\n";
oddNumbers.push_back(9); // Length is 5. Capacity is compiler dependent.
std::cout << "Size: " << oddNumbers.size()
<< " Capacity: " << oddNumbers.capacity() << "\n";
oddNumbers.push_back(11); // Length is 6. Capacity is compiler dependent.
std::cout << "Size: " << oddNumbers.size()
<< " Capacity: " << oddNumbers.capacity() << "\n";
oddNumbers.pop_back(); // Length is 5. Capacity is compiler dependent.
std::cout << "Size: " << oddNumbers.size()
<< " Capacity: " << oddNumbers.capacity() << "\n";
}
Comparing Vectors
Two vectors can be compared using the ==
operator. The operator will first check the vector lengths. If they are of equal length, each pair of elements will be compared using ==
.
std::vector vectorOne{ 3, 1, 4, 1, 5, 9 };
std::vector vectorTwo{ 9, 5, 1, 4, 1, 3 };
if (vectorOne == vectorTwo) {
std::cout << "We should never see this message as the vector elements are not equal.";
}
⚡ Warning:
Vector comparisons depends on the order of the elements.
The above vectors are not equal even though they included the same numbers! If order doesn’t matter you could first sort the vectors, or use something like an unordered_set
instead.
Copying Vectors
The easier way to copy a vector is to use the vector copy constructor:
std::vector vectorOne{ 3, 1, 4, 1, 5, 9 };
std::vector vectorTwo{ vectorOne }; // Copy vectorOne into VectorTwo.
Copying can also be performed using the equals operator:
std::vector vectorOne{ 3, 1, 4, 1, 5, 9 };
std::vector vectorTwo;
vectorTwo = vectorOne; // Copy vectorOne into VectorTwo.
⏳ Wait For It:
With iterators & lambdas you can use copy_if()
to conditionally copy certain elements.
Pointers and Iterators
We can use data()
for direct access to the underlying array using a pointer.
The standard begin()
and end()
iterators (and variants) are all available.
⏳ Wait For It:
These affordances will make more sense once we study pointers and iterators.