Vectors

Steven J. Zeil

Last modified: Oct 26, 2023
Contents:

A vector is similar to an array, except that it can “grow” to accommodate as many items as you actually try to put into it:

First, however, a slight digression into C++-isms. I stated earlier that “A vector is similar to an array”. The similarity comes from the fact that vectors keep their elements in numeric order and allow you to access them via indexing:

int a[];
vector<int> v;
   ⋮
a[i] = v[i+1];
string b[];
vector<string> w;
   ⋮
w[23] = "Hello";
Folderol c[];
vector<Folderol> x;
   ⋮
x[1] = c[1];

Because vectors are templates, we can have vectors containing almost any other type.

Why “almost any type”? You might remember that not every type can be used in an array. A type can only be used as array elements if that type provides a default constructor, because that constructor is used to initialize the elements of the array.

Similarly, types that are to be used as vector elements have a special requirement as well. This requirement is different from, but no more stringent than the requirement for arrays. Types to be used in a vector must supply a copy constructor.

1 The Vector Interface

Let’s take a quick look at how to work with vectors.

1.1 Constructors

 
`vector<int> v1;`

v1 is a vector of integers. Initially, it has 0 elements.

`vector<string> v2(10, "abc");`

v2 is a vector of strings initialized to hold 10 copies of "abc".

vector<string> v3 (containerOfStrings.begin(),
                         containerOfStrings.end());

v3 is initialized to hold a copy of the contents of some other container. That container does not have to be another vector.

 vector<int> v4 = {2, 3, 5, 999};

v4 is initialized using an initializer list.

1.2 Copying

 

These operations replace the entire contents of a vector by something new:

Two ways to copy an entire vector from another vector:

vector<double> v, w;
  ⋮
v = w;
vector<double> z(v);

Other options:

v.assign (container.begin(), container.end());

You can assign all or part of another container to a vector.

 v.assign(24, "abc");

You can also assign some number of copies of a single value to a vector.

1.3 Size & Capacity

 
vector<int>::size_type n;
n = v1.size();

How many elements currently in v1?

v1.resize(n, x);

If v1 has more than n elements, remove the last v1.size()-n elements. If it has fewer than n elements, add n-v1.size() copies of x to the end. (x may be omitted, in which case the added elements are formed using their data type’s default constructor.)

vector<int>::size_type n;
n = v1.capacity();

How many elements total could v1 hold without requesting more memory? (We’ll see later that this affects performance.)

v1.reserve(n);

Add memory, if necessary, to make sure v1 will have room to hold at least n elements. Note that this does not actually change the size() of the vector. It just helps make sure there is enough space for future growth.

1.4 Access to Elements

 
v1[0] = v1[1] + 1;

You can index into a vector just like with arrays. Note that you can only access v[i] if the size() of the vector is already at least i+1.

You can also use the at functions:

v1.at(0) = v1.at(1) + 1;

The difference between at and the square bracket indexing is that access via at is checked to be sure that the index is legal, between 0 and size()-1, inclusive. Normally, this is not checked for square bracket style indexing (though the g++ compiler allows you to request such checking through the use of appropriate compiler flags).

x = v1.front();
v1.front() = v1.front()+1;

Provides access to the first element in the vector.

x = v1.back();
v1.back() = v1.back()+1;

Provides access to the last element in the vector.

1.5 Inserting Elements

 
v2.push_back("def"); 

Adds a new element, "def" to the end of the vector v2. v2.size() increases by 1. This the “normal”, most common way to add elements to a vector.

Although not as efficient, the std::vector also allows:

vector<std::string>::iterator pos;
   ⋮
v2.insert("def", pos);

Inserts a new element, "def", at the indicated position within the vector v2. Any elements already in v2 at that position or higher are moved back one position to make room. v2.size() increases by 1.

1.6 Removing Elements

 
v2.pop_back();

Removes the element at the end of the vector, decreasing the size() by 1.

Although not as efficient, the std::vector also allows:

vector<std::string>::iterator pos;
  ⋮
v2.erase(pos);

Removes the element at the indicated position within the vector v2. Any elements already in v2 at higher positions are moved forward one position to “take up the slack”. v2.size() decreases by 1.

vector<std::string>::iterator start, stop;
  ⋮
v2.erase(start, stop);

Removes the elements at the indicated positions from start up to, but not including stop, within the vector v2. Any elements already in v2 at higher positions are moved forward to “take up the slack”. v2.size() decreases by the number of elements removed.

You can remove all elements from a vector like this:

vector<std::string>::iterator pos;
  ⋮
v2.clear();

1.7 Comparing Vectors

Vectors can be compared using == and < operators. (These operators are not shown in the simplified vector class from your textbook, because they were added to the C++ standard after the book was published.)

Two vectors are equal if they are the same length and if each corresponding pair of elements are equal. For example, if v1 is declared as

vector<int> v1 {1, 2, 3};

then, for varying values of v2, we would have:

v2 v1 == v2 ?
vector<int> v2 {1, 2}; false
vector<int> v2 {1, 2, 3}; true
vector<int> v2 {1, 2, 3, 4}; false
vector<int> v2 {2, 1, 3}; false

The < operator compares vectors according to the idea of lexicographic comparison:

  1. The two vectors are scanned to find two corresponding elements that are not equal.

    Those two non-equal elements are then comapred to see which is less than the other.

  2. If the two vectors have equal contents up to the point where one vector or the other ends, the shorter vector is considered to be less than the other. (If neither vector is shorter than the other, then the two vectors are actually equal, and neither is condidered less than the other.)

This is, in essence, the same set of rules that we conventionally use when comparing strings to put them into “aplhabetic order”.

For example, if v1 is declared as

vector<int> v1 {1, 2, 3};

then, for varying values of v2, we would have:

v2 v1 < v2 ? Why?
vector<int> v2 {1, 2}; false v2 is shorter
vector<int> v2 {1, 2, 3}; false v1 == v2
vector<int> v2 {1, 2, 3, 4}; true v1 is shorter
vector<int> v2 {2, 1, 3}; true v1[0] < v2[0]
vector<int> v2 {1, 1, 3}; false v1[1] > v2[1]
vector<int> v2 {1, 2, 4}; true v1[2] < v2[2]

2 Using Vectors

2.1 Keeping Authors in a Book

Earlier, we developed a Book class that used std::array to hold all of the authors in a book.

abook.h
#ifndef BOOK_H
#define BOOK_H

#include <array>
#include <string>
#include "author.h"

class Publisher;



class Book {
private:
	std::string title;
	std::string isbn;
	std::string publisher;

	static const int MaxAuthors = 10;
	std::array<std::string, MaxAuthors> authors;
	int numAuthors;

public:
	typedef std::array<std::string, MaxAuthors>::iterator iterator;
	typedef std::array<std::string, MaxAuthors>::const_iterator const_iterator;


	Book();

	Book(const std::string& title, const std::string& isbn, const Publisher& publisher,
			Author* authors = nullptr, int numAuthors = 0);

	std::string getTitle() const {return title;}
	void setTitle(std::string theTitle) {title = theTitle;}

	void addAuthor (const Author&);
	void removeAuthor (const Author&);

	std::string getPublisher() const {return publisher;}
	void setPublisher(const Publisher& publ);

	std::string getISBN() const {return isbn;}
	void setISBN(std::string id) {isbn = id;}

	int numberOfAuthors() const;

	iterator begin();
	const_iterator begin() const;
	iterator end();
	const_iterator end() const;


    bool operator== (const Book& right) const;
    bool operator< (const Book& right) const;

};

std::ostream& operator<< (std::ostream& out, const Book& book);


#endif
abook.cpp
/*
 * book.cpp
 *
 *  Created on: May 23, 2018
 *      Author: zeil
 */

#include "book.h"
#include "publisher.h"

#include <algorithm>
#include <cassert>

using namespace std;


Book::Book()
: title(), isbn(),
  publisher(),
  numAuthors(0)
{

}

Book::Book(const std::string& theTitle, const std::string& theISBN, const Publisher& thePublisher,
		Author* theAuthors, int theNumAuthors)
: title(theTitle), isbn(theISBN), publisher(thePublisher.getName()), numAuthors(0)
{
	for (int i = 0; i < theNumAuthors; ++i)
	{
		addAuthor(theAuthors[i]);
	}
}



int Book::numberOfAuthors() const
{
	return numAuthors;
}


void Book::addAuthor (const Author& au)
{
	auto pos = find(authors.begin(), authors.end(), au.getName());
	if (pos == authors.end())
	{
		assert (numAuthors < MaxAuthors);
		authors[numAuthors] = au.getName();
		++numAuthors;
	}
}

void Book::removeAuthor (const Author& au)
{
	auto pos = find(authors.begin(), authors.end(), au.getName());
	if (pos != authors.end())
	{
		auto next = pos;
		++next;
		while (next != authors.end())
		{
			*pos = *next;
			pos = next;
			++next;
		}
		--numAuthors;
	}
}


Book::iterator Book::begin()
{
	return authors.begin();
}
Book::const_iterator Book::begin() const
{
	return authors.begin();
}

Book::iterator Book::end()
{
	return authors.begin()+numAuthors;
}
Book::const_iterator Book::end() const
{
	return authors.begin()+numAuthors;
}




void Book::setPublisher (const Publisher& publ)
{
	publisher = publ.getName();
}


bool Book::operator== (const Book& right) const
{
	return getISBN() == right.getISBN();
}

bool Book::operator< (const Book& right) const
{
	return getISBN() < right.getISBN();
}


std::ostream& operator<< (std::ostream& out, const Book& book)
{
	out << book.getTitle() << ", by ";
	bool firstTime = true;
	for (const string& author: book)
	{
		if (!firstTime)
			out << ", ";
		firstTime = false;
		out << author;
	}
	out << "; " << book.getPublisher() << ", " << book.getISBN();
	return out;
}


Because standard arrays must be constructed with a fixed size, we have to have a separate data member numAuthors to track how many of those array elements are actually in use.

We may be able to change that if we switch to a vector-based implementation.

2.2 Vectorizing the book.h file

Changing the data structure to vector is surprisingly simple:

Here is what we had:

class Book {
private:
    std::string title;
    std::string isbn;
    std::string publisher;

    static const int MaxAuthors = 10;
    std::array<std::string, MaxAuthors> authors;
    int numAuthors;

public:
    typedef std::array<std::string, MaxAuthors>::iterator iterator;
    typedef std::array<std::string, MaxAuthors>::const_iterator const_iterator;

and here is what we change it to:

class Book {
private:
    std::string title;
    std::string isbn;
    std::string publisher;

    std::vector<std::string> authors;

public:
    typedef std::vector<std::string>::iterator iterator;
    typedef std::vector<std::string>::const_iterator const_iterator;

The last point highlights the difference in style between using arrays and using vectors. Most applications of arrays divide the array into used portions and portions available for future expansion. When we work with vectors, however, we only add as many :slots" to the vectors as we have data elements to fill them. (As we will see, the underlying implementation of vector may still use arrays, and those arrays will be divided into used and unused portions. But when we are using vectors that division is almost invisible.)

2.3 Vectorizing the book.cpp file

Probably nothing highlights the difference in styles between these two approaches than the following three functions:

With arrays:

int Book::numberOfAuthors() const
{
	return numAuthors;
}


void Book::addAuthor (const Author& au)
{
	auto pos = find(authors.begin(), authors.end(), au.getName());
	if (pos == authors.end())
	{
		assert (numAuthors < MaxAuthors);
		authors[numAuthors] = au.getName();
		++numAuthors;
	}
}

void Book::removeAuthor (const Author& au)
{
	auto pos = find(authors.begin(), authors.end(), au.getName());
	if (pos != authors.end())
	{
		auto next = pos;
		++next;
		while (next != authors.end())
		{
			*pos = *next;
			pos = next;
			++next;
		}
		--numAuthors;
	}
}

With vectors:

int Book::numberOfAuthors() const
{
	return authors.size();
}


void Book::addAuthor (const Author& au)
{
	auto pos = find(authors.begin(), authors.end(), au.getName());
	if (pos == authors.end())
	{
		authors.push_back(au.getName());
	}
}

void Book::removeAuthor (const Author& au)
{
	auto pos = find(authors.begin(), authors.end(), au.getName());
	if (pos != authors.end())
	{
		authors.erase(pos);
	}
}

We use push_back operations to add new authors to the end of the vector-based collection, and the collection tracks its own size.

Constructors are also simplified slightly because we no longer have to initialize the numAuthors data member, because it no longer exists.

2.4 Vectors copy nicely

We don’t provide our own implementation of the Big 3 in either of these versions, because both std::array and std::vector can be copied & assigned freely, and vectors clean up after themselves with an appropriate destructor.

2.5 Completing the Book operations

The only other difference of note is the provision of the iterators, particularly, the end function.

For arrays the “end” position is the first unused slot in the array:

Book::iterator Book::begin()
{
    return authors.begin();
}
Book::const_iterator Book::begin() const
{
    return authors.begin();
}
Book::iterator Book::end()
{
    return authors.begin() + numAuthors;
}
Book::const_iterator Book::end() const
{
    return authors.begin() + numAuthors;
}

For vectors, all slots are used, so the end position is the position after the last slot:

Book::iterator Book::begin()
{
    return authors.begin();
}
Book::const_iterator Book::begin() const
{
    return authors.begin();
}
Book::iterator Book::end()
{
    return authors.end();
}
Book::const_iterator Book::end() const
{
    return authors.end();
}

The full implementation of the vector-based Book is below:

vbook.h
#ifndef BOOK_H
#define BOOK_H

#include <vector>
#include <string>
#include "author.h"

class Publisher;



class Book {
private:
	std::string title;
	std::string isbn;
	std::string publisher;

	std::vector<std::string> authors;

public:
	typedef std::vector<std::string>::iterator iterator;
	typedef std::vector<std::string>::const_iterator const_iterator;


	Book();

	Book(const std::string& title, const std::string& isbn, const Publisher& publisher,
			Author* authors = nullptr, int numAuthors = 0);

	std::string getTitle() const {return title;}
	void setTitle(std::string theTitle) {title = theTitle;}

	void addAuthor (const Author&);
	void removeAuthor (const Author&);

	std::string getPublisher() const {return publisher;}
	void setPublisher(const Publisher& publ);

	std::string getISBN() const {return isbn;}
	void setISBN(std::string id) {isbn = id;}

	int numberOfAuthors() const;

	iterator begin();
	const_iterator begin() const;
	iterator end();
	const_iterator end() const;


    bool operator== (const Book& right) const;
    bool operator< (const Book& right) const;

};

std::ostream& operator<< (std::ostream& out, const Book& book);


#endif
vbook.cpp
/*
 * book.cpp
 *
 *  Created on: May 23, 2018
 *      Author: zeil
 */

#include "book.h"
#include "publisher.h"

#include <algorithm>
#include <cassert>

using namespace std;


Book::Book()
: title(), isbn(),
  publisher()
{

}

Book::Book(const std::string& theTitle, const std::string& theISBN, const Publisher& thePublisher,
		Author* theAuthors, int theNumAuthors)
: title(theTitle), isbn(theISBN), publisher(thePublisher.getName())
{
	for (int i = 0; i < theNumAuthors; ++i)
	{
		addAuthor(theAuthors[i]);
	}
}



int Book::numberOfAuthors() const
{
	return authors.size();
}


void Book::addAuthor (const Author& au)
{
	auto pos = find(authors.begin(), authors.end(), au.getName());
	if (pos == authors.end())
	{
		authors.push_back(au.getName());
	}
}

void Book::removeAuthor (const Author& au)
{
	auto pos = find(authors.begin(), authors.end(), au.getName());
	if (pos != authors.end())
	{
		authors.erase(pos);
	}
}


Book::iterator Book::begin()
{
	return authors.begin();
}
Book::const_iterator Book::begin() const
{
	return authors.begin();
}

Book::iterator Book::end()
{
	return authors.end();
}
Book::const_iterator Book::end() const
{
	return authors.end();
}




void Book::setPublisher (const Publisher& publ)
{
	publisher = publ.getName();
}


bool Book::operator== (const Book& right) const
{
	return getISBN() == right.getISBN();
}

bool Book::operator< (const Book& right) const
{
	return getISBN() < right.getISBN();
}


std::ostream& operator<< (std::ostream& out, const Book& book)
{
	out << book.getTitle() << ", by ";
	bool firstTime = true;
	for (const string& author: book)
	{
		if (!firstTime)
			out << ", ";
		firstTime = false;
		out << author;
	}
	out << "; " << book.getPublisher() << ", " << book.getISBN();
	return out;
}


3 Required Performance

The C++ standard specifies that a legal (i.e., standard-conforming) implementation of vector must satisfy the following performance requirements:

Operation Speed
vector() O(1)
vector(n, x) O(n)
size() O(1)
v[ i ] O(1)
push_back(x) O(1)
pop_back O(1)
insert O(size())
erase O(size())
front, back O(1)

Next, we will look at how vector actually manages to achieve this performance.