What Is std::vector?

std::vector is the standard library's resizable array. One vector object owns a run of same-typed elements, knows how many it currently owns, and can own a different number later in the same program. When the amount of data is decided while the program runs rather than while you are typing it, this is the container to reach for.

It lives in the <vector> header and is written as a class template, so the element type goes between the angle brackets. std::vector<int> holds int elements, std::vector<double> holds double elements, and no single vector ever mixes the two.

Almost everything about building one comes down to a single question: do you know the values, or only how many there will be? Those two situations use different punctuation, for reasons that catch out a lot of people, so this lesson takes them one at a time and then explains why the punctuation matters.

Starting a vector from values you already have

If the values exist at the point where you define the vector, put them in braces and let the vector size itself:

#include <vector>

int main()
{
    std::vector<int> pending{};                     // nothing stored yet
    std::vector<int> chapterPages{ 18, 24, 9, 31 };
    std::vector shelfHeights{ 30.5, 42.0, 18.25 };

    return 0;
}

Three definitions, three things worth noticing.

pending is value-initialized by the empty braces, which produces a vector of length zero. That sounds useless until you meet the pattern it serves: a container that starts with nothing and grows as data arrives, which is covered later in this chapter under stack behavior.

chapterPages spells out its element type and receives four values, so it ends up four elements long, holding 18, 24, 9 and 31 in that order.

shelfHeights leaves the angle brackets off entirely. The compiler has enough to go on without them: class template argument deduction (CTAD), added in C++17, works the missing parameter out from what sits inside the braces, double in this case. Leaning on CTAD is the preferred style whenever the braces make the type obvious at a glance.

The list constructor does the bookkeeping

The braced, comma-separated run of values in those definitions has a name: it is an initializer list. Container types accept one through a dedicated constructor, the list constructor, which handles three chores so you do not have to:

  • it makes sure enough storage exists for every value in the list
  • it sets the container's length to the number of values in the list
  • it initializes the elements from those values, in the order written

That is the whole mechanism behind std::vector<int> chapterPages{ 18, 24, 9, 31 };. Braces on the right, list constructor on the left, four elements out the other side.

Best Practice
Where the element values are known at the point of definition, brace them straight into the definition. Defining an empty container and filling it on later lines costs extra statements and gives the reader less to go on.

Reaching an element by position

Every element has a position, and operator[] is how you name one. The value inside the square brackets is called a subscript, or informally an index, and counting starts at zero: chapterPages[0] selects the opening element and chapterPages[3] selects the fourth.

Arrays in C++ being zero-based trips people up until you stop reading a subscript as a position number and start reading it as a distance from the front. Move zero elements from the front and you have not moved, so index 0 is the element already under your feet. Move one and you are on the next one along, so index 1 is the second element.

What comes back from operator[] is a reference to the element itself, never a copy of it, so a subscript expression is as good on the left of an assignment as it is on the right:

#include <iostream>
#include <vector>

int main()
{
    std::vector chapterPages{ 18, 24, 9, 31 };

    std::cout << "Opening chapter: " << chapterPages[0] << " pages" << '\n';
    std::cout << "Closing chapter: " << chapterPages[3] << " pages" << '\n';

    chapterPages[2] = 12;
    std::cout << "Chapter three after trimming: " << chapterPages[2] << " pages" << '\n';

    return 0;
}

Output:

Opening chapter: 18 pages
Closing chapter: 31 pages
Chapter three after trimming: 12 pages
Key Concept
`chapterPages[2]` is not a snapshot of the third element, it is the third element. Reading through it copies nothing, and writing through it goes straight into the vector's storage.

Indices the vector does not have

A vector of length N answers to exactly N subscripts, 0 through N - 1. Hand operator[] anything else and you get undefined behavior, because it does no bounds checking whatsoever. It works out where such an element would sit and returns a reference to that spot, without ever asking whether an element actually lives there. The program might print nonsense, might quietly damage an unrelated variable, might crash on the spot, and might appear to work for months.

Negative subscripts are easy enough to steer clear of. The one that catches experienced programmers is the length itself.

Warning
The length of a vector is never a valid subscript for it. A four-element vector answers to 0, 1, 2 and 3; `chapterPages[4]` is one step past the last element and is undefined behavior, even though the expression compiles without a murmur.

Some standard library implementations soften this during development. Visual Studio's, for instance, builds an index check into debug configurations, so an out-of-range subscript trips an assertion while you are testing rather than silently misbehaving. Release configurations compile that check away, so the shipped program pays nothing for it. A checked accessor that works in every configuration, at(), is the subject of the next lesson.

One unbroken block of memory

Elements of an array are laid out end to end: adjacent in memory, with no gaps and no per-element bookkeeping stashed between them. You can watch the arithmetic work out:

#include <iostream>
#include <vector>

int main()
{
    std::vector chapterPages{ 18, 24, 9, 31 };

    std::cout << "One entry occupies " << sizeof(int) << " bytes" << '\n';
    std::cout << "Four entries occupy " << sizeof(int) * chapterPages.size() << " bytes" << '\n';

    return 0;
}

Output:

One entry occupies 4 bytes
Four entries occupy 16 bytes

Four int elements take exactly four ints worth of space, which is only possible if they sit shoulder to shoulder with nothing wedged in between.

That layout is what makes subscripting cheap. The vector knows where its storage begins and how wide one element is, so element i is found by computing start + i * element size: one multiply, one add, done. Reaching the last element of a ten-thousand element vector costs exactly what reaching the first one costs. Containers with that property support random access, meaning any element can be reached directly instead of being walked to one link at a time, and it is the main reason arrays are the default choice among containers.

Starting a vector before you have the values

Now the other situation. Suppose you need somewhere to put a rainfall reading for each hour of the day: twenty-four slots must exist before the first reading arrives. Typing twenty-four zeros between braces would work, and would be tedious to write, hard to count, and annoying to change. std::vector has a constructor that takes the length instead:

#include <iostream>
#include <vector>

int main()
{
    std::vector<double> hourlyRainfall(24);

    hourlyRainfall[6] = 1.5;

    std::cout << "Slot 0 reads " << hourlyRainfall[0] << '\n';
    std::cout << "Slot 6 reads " << hourlyRainfall[6] << '\n';

    return 0;
}

Output:

Slot 0 reads 0
Slot 6 reads 1.5

Every slot is value-initialized on the way in, which zeroes arithmetic types and runs the default constructor for class types. Note the parentheses. They are not a stylistic preference here; braces would give you something else entirely, which is the subject of the next section.

Braces and parentheses do not mean the same thing here

Two definitions that differ by nothing but punctuation, and a program that shows what each one produced:

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> braced{ 6 };
    std::vector<int> parens( 6 );

    std::cout << "braced holds " << braced.size() << '\n';
    std::cout << "parens holds " << parens.size() << '\n';

    return 0;
}

Output:

braced holds 1
parens holds 6

Both constructors were candidates for { 6 }. Read the braces as an initializer list and the list constructor applies, giving one element whose value is 6. Read them as a single argument and the length constructor applies, giving six elements set to 0. An ambiguity like that would normally be a compilation error, but C++ has a tie-breaker for it: braces holding at least one value are handed to the list constructor whenever the class has one, in preference to anything else that would fit. Without that rule, giving a class a list constructor would make every single-argument construction ambiguous.

Key Concept
Braces with something in them prefer a matching list constructor over any other constructor. Empty braces are the exception: with no values to construct from, the default constructor wins.

The full set of forms, using std::vector<int> as the example type:

What you write What you get
std::vector<int> box = 6; rejected, because copy initialization cannot select the explicit length constructor
std::vector<int> box(6); six elements, each value-initialized to 0
std::vector<int> box{ 6 }; one element holding 6
std::vector<int> box = { 6 }; one element holding 6
std::vector<int> box{}; no elements
std::vector<int> box = {}; no elements
Best Practice
Look at what the number in the initializer stands for. Element data belongs in braces. A length, or any other argument that is not element data, belongs in parentheses, because braces will route it to the list constructor behind your back.
Warning
Which constructor non-empty braces select depends on whether a list constructor exists at all. Adding one to a class that previously had none silently changes the meaning of every definition of that class already written with non-empty braces.

A vector as a class member

Parentheses have one place they cannot go: the default initializer of a data member.

The line below does not compile.

struct ReadingLog
{
    std::vector<int> dailyPages(7); // rejected
};

Inside a class body, a parenthesis after a member name begins a parameter list, so the compiler starts reading that line as a member function declaration and gives up when it reaches the 7, reporting expected identifier before numeric constant. CTAD is off the table in this position too, so the element type has to be spelled out.

Braces are accepted, though, and a braced initializer may contain a temporary built with parentheses:

#include <vector>

struct ReadingLog
{
    std::vector<int> dailyPages{ std::vector<int>(7) };
};

int main()
{
    ReadingLog log{};

    return 0;
}

The inner definition builds a seven-element vector as a temporary, and the outer braces hand that temporary over as the initializer for dailyPages.

const vectors, and the constexpr question

A vector can be const:

#include <vector>

int main()
{
    const std::vector<int> chapterPages{ 18, 24, 9, 31 };

    return 0;
}

Such a vector has to receive its values where it is defined, since nothing can be assigned to it afterwards, and its elements behave as const as well.

Do not try to reach the same place by const-ing the element type. std::vector<const int> fails a static assertion inside the header, which reports that std::vector must have a non-const, non-volatile value_type. Constness is applied to the container, not to what the container holds.

constexpr is a different story, and one that is often stated too bluntly. There is no such thing as a constexpr std::vector object: the elements live in storage the vector allocates, and a constexpr object cannot hold on to an allocation once compilation has finished. GCC rejects the attempt with a message saying the initializer is not a constant expression because it refers to a result of 'operator new'.

What C++20 did change is that the member functions themselves are constexpr, so a vector may be created, used and destroyed entirely inside a single constant evaluation:

#include <vector>

constexpr int longestTwo()
{
    std::vector<int> scratch{ 18, 24, 9, 31 };
    return scratch[1] + scratch[3];
}

static_assert(longestTwo() == 55);

int main()
{
    return 0;
}

That compiles, and the sum is computed before the program ever runs. The restriction is on the object outliving the evaluation, not on the container being used at compile time. When what you need is a constant array that persists, std::array is the type for the job.

Where the name came from

In everyday mathematics a vector is a quantity with magnitude and direction, which describes nothing at all about a growable array of ints. The name is inherited rather than descriptive. Alexander Stepanov, who designed the STL, borrowed it from Scheme and Common Lisp, and has since written that doing so clashed with a much older meaning and that the container should have been named array. By the time that was said out loud, std::vector was in millions of lines of code, and there it stays.

Summary

  • std::vector is the standard library's dynamic array, declared in <vector> as a class template whose parameter is the element type.
  • Braces containing values invoke the list constructor, which reserves the storage, sets the length to the number of values, and initializes the elements in order. Empty braces produce a vector of length zero.
  • CTAD lets you drop the angle brackets when the initializers pin down the element type, except in a member's default initializer, where the type must be written out.
  • operator[] takes a zero-based subscript and returns a reference to the element, so you can read and write through it. It performs no bounds checking, and any subscript outside 0 to N - 1, the length included, is undefined behavior.
  • Elements occupy one contiguous block with no per-element overhead, which is what allows any element to be reached by a single address calculation: random access at a fixed cost.
  • Parentheses around a count invoke the length constructor, giving that many value-initialized elements. Braces around the same count give a one-element vector, because non-empty braces prefer a matching list constructor.
  • Constness belongs on the container itself; a vector of const elements is not a legal type. No vector object can be constexpr either, though a vector may be built and consumed inside a constant expression; use std::array for a constant array that has to persist.

Get the punctuation right and the rest of std::vector follows easily, which is a large part of why it is the container most C++ programs reach for first.