2.6. The vector class

A vector is intended to behave like a dynamically sized array. It is a template, so unlike a string, which is a container for characters only, a vector can serve as a container for any type. More on templates later, for now, we just need to know enough to know how declare a vector.

As with strings, in standard C, the typical way to work with a collection of data is with a 'raw' array:

int a[] = {3, 1, 4, 1, 5, 9};

Some downsides to raw arrays are that they:

  • Do not know their own size

  • Need to have their size specified when declared

  • Decay into pointers easily

  • Provide no convenience functions

The vector class solves these problems for us and a few others besides. Declaring a vector is quite similar to the string declarations from the previous section. In order to access the STL vector capabilities, use #include <vector>.

Declare

To properly declare a vector, the type of data stored in the vector must be declared as a type parameter.

The <int> and <std::string> represent the template parameters passed to the vector. It is these template parameters that allow the vector class to serve as a container for (almost) any type. There are some limits we will cover later, but for now, know that any normal type you already have learned about can be stored in a vector.

// an empty vector of int
vector<int> x;

// initialize and store: "x", "x"
vector<std::string> dos_equis (2, "x");

// C++11 initialization list syntax
vector<int> pi_digits = {3,1,4,1,5,9};

Unlike a fundamental type, the declaration vector<int> x; does not create an uninitialized variable. It creates a fully formed vector with no elements stored in it yet. This is perfectly OK and normal.

However, a common error is to forget to include the template parameter:

vector x;        // compile error

Run It

 1#include <iostream>
 2#include <string>
 3#include <vector>
 4
 5using std::vector;       // alias type std::vector
 6
 7int main() {
 8  vector<int> x;                          // empty vector of int
 9
10  for (const auto& value: x)
11  {
12    std::cout << value << ',';
13  }
14  std::cout << '\n';
15
16  vector<std::string> dos_equis (2, "x");  // "x", "x"
17
18  vector<int> pi_digits = {3,1,4,1,5,9};  // C++11
19  for (const auto& value: pi_digits)
20  {
21    std::cout << value << ',';
22  }
23  std::cout << '\n';
24
25  return 0;
26}

Given a vector declared as:

std::vector<int> v(4);

A container capable of storing 4 integers is created:

digraph { node [ fontname = "Courier" fontsize = 14 shape = "record" style=filled fillcolor=lightblue ] names [ color = white; fillcolor=white; label = "{size: | <f0> data: }"; ] struct [ label = "{4 | <f0> }"; ] node [shape=record, color=black, fontcolor=black, fillcolor=white, width=3.75, fixedsize=true]; labels [label="<f0> | <f4> size", color=white]; values [label="<f0> v[0] | <f1> v[1] | <f2> v[2] | <f3> v[3]", color=black, fillcolor=lightblue, style=filled]; edge [color=black]; struct:f0:s -> values:f0; labels:f4 -> values:f3; {rank=same; struct,labels}; }

Although the vector object is initialized, its contents are not. Many compiler implementations will initialize the contents to zero, but don't rely on this behavior. Explicitly initialize with a default value, if that is what you want:

std::vector<int> v(4, -1);

A vector comes with a rich assortment of convenience functions. Like an array, the operator[] can be used to access elements without bounds checking. Like a string, the at function provides bounds checking and will throw a std::out_of_range exception if an out of bounds index is used on the vector.

Access operations

Like arrays, indexes are zero-based.

// read vector elements
std::cout << "First element: " << numbers[0];
std::cout << "First element: " << numbers.at(0);

// write vector elements
numbers[0] = 5;
numbers.at(0) = 5;

A common source of error occurs when printing a vector. A vector feels like a built-in type and this seems like it should work:

// compile error
std::cout << "all numbers: " << numbers;

The vector type does not 'know' how to send it's values to an output stream by default.

Something to consider

Why do you think this feature is not built into the standard library?

Run It

This example demonstrates an out of range error.

How can we fix this while changing the least amount of code possible?

 1#include <iostream>
 2#include <vector>
 3
 4int main() {
 5  std::vector<int> numbers {2, 4, 6, 8};
 6  std::cout << "Size: " << numbers.size() << '\n';
 7  std::cout << "Second element: " << numbers[1] << '\n';
 8
 9  numbers.at(0) = 5;
10  numbers.at(4) = numbers[3] + 2;  // out of range error.
11                                   // index 4 is out of bounds
12
13  std::cout << "All numbers:";
14  for (const auto& num : numbers) {
15    std::cout << ' ' << num;
16  }
17  std::cout << '\n';
18  return 0;
19}

Fill and print vector

A vector 'hello world':

  1. Fill a vector in one simple way, using push_back

  2. iterate through the vector and print

 1#include <iostream>
 2#include <string>
 3#include <vector>
 4
 5using std::cout;
 6using std::string;
 7using std::vector;
 8
 9
10int main() {
11  vector<string> words = {
12     "reach", "clear", "fall", "set", "yard",
13     "liquid", "wise", "badge", "four", "coherent"
14  };
15
16  cout << "Word list: \n";
17  for (string s: words) {
18    cout << s << ", \thas " << s.size() << " letters\n";
19  }
20}

The vector class also provides:

front and back

return a reference to the first and last elements

size

return the number of elements

empty

return true if the container is empty

Although there are more functions, these are the ones we need to worry about for now. We will be looking more at memory management in vectors in The std::vector class.

Something to consider

What is the difference between a std::string and std::vector<char>?

Why did the developers of the STL decide it was important to include both?

Comparisons between vectors are also automatically handled by the class. In the case of a vector, operator==, or an equality comparison between two vectors a and b, means the two vectors are equal if a.size() == b.size() and each element in a compares equal with each element in b in the same position in the vector.

Compare operations

Vectors support the same syntax as the built in types.

// declare 2 vectors, one empty and one not

std::vector<int> x {2, 4, 6, 8};
std::vector<int> y;

bool test = (x == y);   // test is false

y = x;

Run It

 1#include <vector>
 2#include <iostream>
 3
 4int main() {
 5  std::vector<int> x {2, 4, 6, 8};
 6  std::vector<int> y;
 7
 8  if (x == y) {
 9    std::cout << "x and y are equal\n";
10  } else {
11    std::cout << "x and y differ\n";
12  }
13
14  y = x;          // copy all data from x into y
15  if (x == y) {
16    std::cout << "x and y are equal\n";
17  } else {
18    std::cout << "x and y differ\n";
19  }
20
21  return 0;
22}

Try This!

Create two vectors of strings containing the same values and check them for equality.

1#include <iostream>
2
3int main() {
4}

2.6.1. Adding data to a vector

How do we solve the out_of_range exception from a few examples ago? How do we dynamically add data to a vector?

A simple way is to use the push_back function.

Given an vector of 3 int's:

digraph { node [ fontname = "Courier" fontsize = 14 shape = "record" style=filled fillcolor=lightblue ] names [ color = white; fillcolor=white; label = "{size: | <f0> data: }"; ] struct [ label = "{3 | <f0> }"; ] node [shape=record, color=black, fontcolor=black, fillcolor=white, width=3.75, fixedsize=true]; labels [label="<f0> | <f2> size", color=white]; values [label="<f0> v[0]\n= 10 | <f1> v[1]\n= 20 | <f2> v[2]\n= 30", color=black, fillcolor=lightblue, style=filled]; edge [color=black]; struct:f0:s -> values:f0; labels:f2 -> values:f2; {rank=same; struct,labels}; }

values.push_back(40);

Appends the value 40 to the end of the vector.

digraph { node [ fontname = "Courier" fontsize = 14 shape = "record" style=filled fillcolor=lightblue ] names [ color = white; fillcolor=white; label = "{size: | <f0> data: }"; ] struct [ label = "{4 | <f0> }"; ] node [shape=record, color=black, fontcolor=black, fillcolor=white, width=3.75, fixedsize=true]; labels [label="<f0> | <f4> size", color=white]; values [label="<f0> v[0]\n= 10 | <f1> v[1]\n= 20 | <f2> v[2]\n= 30 | <f3> v[3]\n= 40", color=black, fillcolor=lightblue, style=filled]; edge [color=black]; struct:f0:s -> values:f0; labels:f4 -> values:f3; {rank=same; struct,labels}; }

push_back and pop_back

push_back appends an element to the end and increases the capacity of the vector, if needed.

pop_back reduces the size of the vector by one. The last element is no longer available.

Note that pop_back does not return a value.

If you need that last element, remember to save it first.

std::vector<char> letters {'a', 'b', 'c'};

letters.push_back('d');  // add 'd' to the end of the vector
letters.pop_back();      // pop_back is the opposite:

Run It

 1#include <vector>
 2#include <iostream>
 3
 4int main() {
 5  std::vector<char> letters {'a', 'b', 'c'};
 6
 7  letters.at(0) = 'z';
 8  letters.push_back('d');  // add 'd' to the end of the vector
 9  char ch = 'e';
10  letters.push_back(ch);  // add 'e' to the end
11  letters.pop_back();     // pop_back is the opposite:
12                          //  - removes the end element from the vector
13
14  std::cout << "All letters:";
15  for (const auto& c : letters) {
16    std::cout << ' ' << c;
17  }
18  std::cout << '\n';
19  letters.clear();         // clear all contents from vector
20  return 0;
21}

2.6.2. Vector capacity

A vector exposes an interface that 'feels like' an array, but the underlying storage grows to accommodate new data as required. With an array, you either have to allocate as much memory as you might need in the worst case, even if only a small fraction is used most of the time or you allocate 'just enough' and when more memory is required, copy all the data into a new array. A vector does do this also, but the implementation is hidden and you don't have to worry about it.

Something to be aware of - when pop_back is called, no actual storage is deleted. The memory is still available in the vector and available for reassignment with pop_back.

This extra memory after the current size is referred to as the total capacity of the vector, or just capacity.

digraph { node [ fontname = "Courier" fontsize = 14 shape = "record" style=filled fillcolor=lightblue ] names [ color = white; fillcolor=white; label = "{size: | <f0> data: }"; ] struct [ label = "{4 | <f0> }"; ] node [shape=record, color=black, fontcolor=black, fillcolor=white, width=3.75, fixedsize=true]; labels [label="<f0> | <f4> size | <f5> spare\ncapacity ", color=white]; values [label="<f0> v[0]\n= 1 | <f1> v[1]\n= 1 | <f2> v[2]\n= 1 | <f3> v[3]\n= 1 | | <f5> ", color=black, fillcolor=lightblue, style=filled]; edge [color=black]; struct:f0:s -> values:f0; labels:f4 -> values:f3; labels:f5 -> values:f5; {rank=same; struct,labels}; }

Managing the storage capacity in addition to the vector data is one of the things that make vectors efficient.

Phrase O'matic

The 'phrase-o-matic' is a port of a fun little java program from Head First Java, 2nd ed. ISBN-13: 978-0596009205

 1#include <iostream>
 2#include <random>
 3#include <string>
 4#include <vector>
 5
 6int main() {
 7  // initialize a random number generator
 8  std::random_device r;
 9  std::default_random_engine eng(r());
10  using rand = std::uniform_int_distribution<std::uint64_t>;
11
12  const char* list_one[] = {
13    "24/7", "multi-tier", "30,000 foot", "B-to-B", "win-win",
14    "front-end", "web-based", "pervasive", "smart", "six-sigma",
15    "critical-path", "dynamic", "extreme", "three-tier", "agile"
16  };
17
18  // we prefer vector over arrays
19  const std::vector<std::string> list_two = {
20    "empowered", "sticky", "value-added", "oriented", "centric",
21    "distributed", "clustered", "branded", "outside-the-box",
22    "positioned", "networked", "focused", "leveraged", "aligned",
23    "targeted", "shared", "cooperative", "accelerated"
24  };
25
26  const std::vector<std::string> list_three = {
27    "process", "tipping-point", "solution", "architecture",
28    "core competency", "strategy", "mind-share", "portal",
29    "space", "vision", "paradigm", "mission"
30  };
31
32  const std::size_t one_size = 14;  // arrays don't know their size
33  auto r1 = rand {0, one_size} (eng);
34  auto r2 = rand {0, list_two.size()-1} (eng);   // vectors know their size
35  auto r3 = rand {0, list_three.size()-1} (eng);
36
37  std::string phrase = {list_one[r1]};
38  phrase += " " + list_two[r2] + " " + list_three[r3];
39
40  std::cout << "What we need is a " << phrase << '\n';
41
42  // or could have omitted temporary phrase and simply:
43  // std::cout << "What we need is a " << list_one[r1] << ' '
44  //                                   << list_two[r2] << ' '
45  //                                   << list_three[r3] << '\n';
46  return 0;
47}

vector test

A short test program to demonstrate the parts of the vector interface.

What other tests can you make?

 1#include <iomanip>
 2#include <iostream>
 3#include <string>
 4#include <vector>
 5
 6using std::string;
 7using std::vector;
 8
 9vector<string> make_vector() {
10  return {
11     "reach", "clear", "fall", "set", "yard",
12     "liquid", "wise", "badge", "four", "coherent"
13   };
14}
15
16void check (const string& name, const string& actual, const string& expected)
17{
18  std::cout << std::left << std::setfill('.')
19            << std::setw(50) << name
20            << std::setw(7) <<  std::left;
21  if(actual == expected) {
22    std::cout << " OK      \n";
23    return;
24  }
25  std::cout << " FAILED\n";
26  std::cout << "\treceived [" << actual
27            << "], but expected [" << expected << "]\n";
28  exit(1);
29}
30
31// write test cases
32void test_simple_access(vector<string> words) {
33   check("test at()", words.at(0), "reach");
34   check("test operator[]", words[0], "reach");
35   check("test at() end", words.at(9), "coherent");
36   check("test operator[]", words[9], "coherent");
37
38   // foo.at(i) and foo.[i] refer to the same thing
39   for (auto i=0U; i< 3; ++i) {
40   // for (auto i=0U; i< words.size(); ++i) {
41      check("at and operator[] are the same", words.at(i), words[i]);
42   }
43
44   const string empty;
45   // Try uncommenting these lines to see what happens
46   //check("test at out of bounds", words.at(-1), empty);
47   //check("test at out of bounds", words.at(11), empty);
48   //check("test [] out of bounds", words[-1], empty);
49   //check("test [] out of bounds", words[11], empty);
50}
51
52void test_other_access(vector<string> words) {
53   check("test front()", words.front(), words.at(0));
54   check("test back()", words.back(), words.at(words.size()-1));
55}
56
57void test_assignment(vector<string> words) {
58   check("test original value", words[2], "fall");
59   words[2] = "falldown";
60   check("test new value", words[2], "falldown");
61}
62
63// call test cases
64int main() {
65  auto words = make_vector();
66
67  test_simple_access(words);
68  test_other_access(words);
69  test_assignment(words);
70}

More to Explore