Introduction to std::vector and List Constructors

In the previous lesson, we introduced containers and arrays. This lesson focuses on std::vector, the dynamic array type you'll use throughout your C++ programming.

Introduction to std::vector

std::vector is a container class in the C++ standard library that implements a dynamic array. Defined in the <vector> header as a class template, it takes a template type parameter defining the element type. Thus, std::vector<int> declares a vector whose elements are integers.

Creating a std::vector is straightforward:

#include <vector>

int main()
{
    // Value initialization (uses default constructor)
    std::vector<int> empty{}; // vector containing 0 int elements

    return 0;
}

Variable empty is a std::vector with element type int. Using value initialization, our vector starts empty (with no elements).

An empty vector may not seem useful now, but we'll encounter this pattern frequently when working with dynamic data.

Initializing with a list of values

Since containers manage sets of related values, we typically initialize them with those values using list initialization:

#include <vector>
#include <string_view>

int main()
{
    // List construction (uses list constructor)
    std::vector<int> scores{ 85, 92, 78, 90 };                           // 4 int elements
    std::vector names{ "Alice"sv, "Bob"sv, "Carol"sv };                  // 3 std::string_view elements using CTAD

    return 0;
}

With scores, we explicitly specify std::vector<int>. The 4 initialization values create a vector with elements 85, 92, 78, and 90.

With names, we use C++17's CTAD (class template argument deduction) to deduce the element type from initializers. The 3 values create a vector with elements "Alice", "Bob", and "Carol".

List constructors and initializer lists

An initializer list is a braced list of comma-separated values (e.g., { 1, 2, 3 }).

Containers typically have a list constructor that constructs instances from initializer lists. The list constructor:

  • Ensures the container has enough storage for all initialization values
  • Sets the container's length to the number of elements in the initializer list
  • Initializes elements to the values in sequential order

When you provide an initializer list, the list constructor is called.

Best Practice
Use list initialization with an initializer list of values to construct a container with those element values.

Accessing elements with the subscript operator

The most common way to access array elements is using the subscript operator (operator[]). Inside the brackets, provide an integral value (the subscript or index) identifying which element you want.

Arrays in C++ are zero-based: the first element has index 0, the second has index 1, and so on.

#include <iostream>
#include <vector>

int main()
{
    std::vector values{ 10, 25, 50, 100, 200 }; // 5 int elements

    std::cout << "First value: " << values[0] << '\n';
    std::cout << "Second value: " << values[1] << '\n';
    std::cout << "Sum of first three: " << values[0] + values[1] + values[2] << '\n';

    return 0;
}

Output:

First value: 10
Second value: 25
Sum of first three: 85

The subscript operator returns a reference to the actual element, not a copy. You can use it just like a normal object.

Key Concept
Indexes are distances (offsets) from the first element. Starting at the first element and traveling 0 elements leaves you on the first element (index 0). Traveling 1 element reaches the second element (index 1).

Subscript out of bounds

The subscript must select a valid element. For an array of length N, valid indices are 0 through N-1.

operator[] performs no bounds checking—it doesn't verify the index is valid. Invalid indices cause undefined behavior.

Warning
In an array with N elements, the first element has index 0, the last has index N-1. There is no element with index N! Using N as a subscript causes undefined behavior.

Some compilers (like Visual Studio) provide debug-mode assertions that catch invalid indices. In release mode, these assertions are removed for performance.

Arrays are contiguous in memory

Array elements are always allocated contiguously—adjacent in memory with no gaps between them.

#include <iostream>
#include <vector>

int main()
{
    std::vector values{ 10, 25, 50, 100, 200 };

    std::cout << "An int is " << sizeof(int) << " bytes\n";
    std::cout << &(values[0]) << '\n';
    std::cout << &(values[1]) << '\n';
    std::cout << &(values[2]) << '\n';

    return 0;
}

Sample output:

An int is 4 bytes
0x1B4F720
0x1B4F724
0x1B4F728

The memory addresses are 4 bytes apart—the size of an int. This means arrays have no per-element overhead and allow random access (any element can be accessed directly rather than sequentially). This efficiency is a primary reason arrays are often preferred over other containers.

Constructing a vector of specific length

Sometimes you need a vector of a specific size before having values to fill it. You could use placeholder values:

    std::vector<int> data{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; // 10 zeros

But this is tedious and error-prone. Instead, use the explicit length constructor:

    std::vector<int> data(10); // 10 int elements, value-initialized to 0

Each element is value-initialized (zero for int, default constructor for class types).

This constructor requires direct initialization (parentheses, not braces).

Non-empty initializer lists prefer list constructors

Consider this definition:

    std::vector<int> data{ 10 }; // what does this create?

Two constructors could match:

  • { 10 } as an initializer list, matched with the list constructor (creates a 1-element vector containing value 10)
  • { 10 } as a single value, matched with the length constructor (creates a 10-element vector of zeros)

C++ has a special rule: when an initializer list is non-empty, a matching list constructor is preferred over other constructors. So { 10 } creates a 1-element vector containing 10.

Key Concept
When constructing a class type object using an initializer list: - Empty initializer list: default constructor is preferred over list constructor - Non-empty initializer list: matching list constructor is preferred over other constructors

Examples:

    // Copy init
    std::vector<int> v1 = 10;     // Error: copy init won't match explicit constructor

    // Direct init
    std::vector<int> v2(10);      // 10 elements, value-initialized to 0

    // List init
    std::vector<int> v3{ 10 };    // 1 element containing value 10

    // Copy list init
    std::vector<int> v4 = { 10 }; // 1 element containing value 10

    // Default init
    std::vector<int> v5{};        // Empty vector (0 elements)
    std::vector<int> v6 = {};     // Empty vector (0 elements)
Best Practice
When constructing a container with arguments that are not element values (like a length), use direct initialization with parentheses.

Tip: When std::vector is a class member and needs a default size:

struct GameData
{
    std::vector<int> scores{ std::vector<int>(5) }; // 5 elements, value-initialized
};

Direct initialization isn't allowed for member default initializers, so create a temporary vector with direct initialization and use it as the initializer.

Const and constexpr std::vector

std::vector can be const:

#include <vector>

int main()
{
    const std::vector<int> values{ 10, 25, 50, 100, 200 }; // cannot be modified

    return 0;
}

A const std::vector must be initialized and cannot be modified afterward. Its elements are treated as const.

The element type must not be const itself (e.g., std::vector<const int> is disallowed). A container's const-ness comes from const-ing the container, not the elements.

One significant limitation: std::vector cannot be constexpr. For constexpr arrays, use std::array instead.

Why is it called a vector?

In mathematics, "vector" typically means an object with magnitude and direction. So why is std::vector named this way?

Alexander Stepanov, one of the STL designers, wrote: "The name vector in STL was taken from the earlier programming languages Scheme and Common Lisp. Unfortunately, this was inconsistent with the much older meaning of the term in mathematics... this data structure should have been called array."

So std::vector is arguably misnamed, but it's too late to change.

Summary

std::vector basics: A dynamic array container defined in <vector> as a class template. Construct empty with std::vector<T>{} or with values using an initializer list.

List constructors: Containers have list constructors that construct instances from initializer lists, allocating storage, setting length, and initializing elements in order.

Subscript operator: Access elements with operator[] using zero-based indices. Returns a reference to the element. No bounds checking—invalid indices cause undefined behavior.

Contiguous memory: Array elements are stored adjacently in memory with no gaps, enabling efficient random access to any element.

Length constructor: Use direct initialization std::vector<T>(n) to create a vector with n value-initialized elements. Cannot use braces—that invokes the list constructor.

List constructor precedence: Non-empty initializer lists prefer list constructors over other constructors. { 10 } creates a 1-element vector, not a 10-element vector.

Const vectors: Can be const but not constexpr. Element type must not be const—const-ness comes from the container, not elements. Use std::array for constexpr arrays.

std::vector is your go-to container for dynamic arrays. Its flexibility, ease of use, and efficiency make it one of the most commonly used types in C++.