KAHIBARO
Discord Login Register

2.7 Arrays and Vectors

C++ arrays

In C++ you often need to store many values of the same type under a single variable name. The simplest tool for this is the built-in C++ array. An array is a fixed-size, contiguous block of memory that holds elements of a single type, for example several integers or several doubles.

A C++ array is declared by specifying the element type, a name, and the number of elements in square brackets. For example:

cpp
int counts[10];      // 10 integers
double energy[100];  // 100 doubles

The size inside the brackets must be a constant expression known at compile time. This is very important for ROOT users who often read data into arrays. You cannot change the size later. Once you declare double energy[100];, that array always has space for exactly 100 elements.

You can also initialize an array when you declare it:

cpp
int numbers[5] = {1, 2, 3, 4, 5};
double binEdges[4] = {0.0, 1.0, 2.0, 3.0};

If you provide fewer initial values than the declared size, the remaining elements are set to zero:

cpp
int flags[4] = {1, 0};  // flags = {1, 0, 0, 0}

Because C++ arrays are just raw blocks of memory, they do not know their own size. The compiler knows the size at compile time, but at runtime you must be careful to stay within the valid index range. This is less safe than higher-level containers and is a common source of bugs.

Arrays are very fast and are often used in performance-critical parts of ROOT code or to interface with ROOT classes that expect raw pointers, for example TGraph created from arrays of x and y values. However, for most general-purpose analysis it is better to use std::vector, which provides safety and flexibility.

std::vector

std::vector is the standard C++ dynamic array type. It behaves like a resizable array: it stores elements contiguously in memory, but can grow or shrink during the program. This makes it much more flexible and safer than a raw C++ array, especially when the number of elements is not known in advance.

To use std::vector you must include the <vector> header and place the type name inside angle brackets:

cpp
#include <vector>
std::vector<int> hits;          // vector of integers
std::vector<double> energies;   // vector of doubles

A freshly declared vector is empty. You can add elements at the end using push_back:

cpp
energies.push_back(1.23);
energies.push_back(2.34);
energies.push_back(3.45);

After these calls, energies has size 3.

You can also construct a vector with an initial size and optionally a default value:

cpp
std::vector<double> values(10);         // 10 doubles, all initialized to 0.0
std::vector<int> flags(5, -1);          // 5 integers, all initialized to -1
std::vector<int> ids = {11, 13, 211};   // list initialization (C++11)

The most useful properties of std::vector in ROOT analysis are:

  1. Dynamic size. You can add or remove elements at runtime, which is important when reading events that have variable multiplicities, such as a different number of detector hits per event.
  2. Safe access helpers. You can query the current size with size(), and you can use methods like at() that perform bounds checking.
  3. Integration with ROOT. Many ROOT classes know how to work directly with std::vector, especially TTrees that store physics objects like std::vector<float> for per-event measurements.

std::vector provides several important member functions:

Function / propertyMeaning
v.size()Number of elements currently stored
v.empty()Returns true if v.size() == 0
v.push_back(x)Append element x to the end
v.clear()Remove all elements
v.resize(n)Change size to n elements
v[i]Access element at index i (no bounds check)
v.at(i)Access element with bounds checking
v.front()First element
v.back()Last element

Important: std::vector indices run from 0 to size() - 1. Accessing v[size()] or any negative index is out of bounds and leads to undefined behavior.

In ROOT macros you will often use std::vector<double> or std::vector<float> to hold values to be plotted, passed to graphs, or stored in TTrees. Compared with raw arrays, std::vector is usually the recommended default choice unless there is a specific reason to use built-in arrays.

Accessing elements

Both C++ arrays and std::vector are indexed using square brackets. The index of the first element is 0, not 1. For an array or vector of size N, the valid indices are:
$$
0,\, 1,\, 2,\, \dots,\, N-1.
$$

For a built-in array:

cpp
double energy[4] = {1.0, 2.0, 3.0, 4.0};
double first  = energy[0];  // 1.0
double second = energy[1];  // 2.0
energy[2] = 5.0;            // change third element to 5.0

There is no bounds checking. If you write energy[10] = 0.0; you overwrite memory that does not belong to the array. This can corrupt data or crash your program.

For a std::vector you can use the same syntax:

cpp
std::vector<double> energy = {1.0, 2.0, 3.0, 4.0};
double first  = energy[0];
double last   = energy[3];
energy[1] = 2.5;  // modify second element

This is fast but still has no automatic bounds checking. If you want safety, you can use the at() member function:

cpp
double value = energy.at(1);      // ok
double bad   = energy.at(10);     // throws an exception (runtime error)

In short scripts and ROOT macros, operator[] is common, but when you debug or learn, at() can help you detect mistakes earlier.

For vectors you also frequently need the current number of elements:

cpp
std::vector<double> energies;
// fill the vector
std::size_t n = energies.size();  // size_t is an unsigned integer type

You can use this size in loops, histograms filling, or when passing data to other ROOT objects. Always use size() instead of hard-coding the length. This is more robust and avoids bugs when the number of elements changes.

When you work with ROOT, you might pass array data to classes that require pointers. A vector can provide a pointer to its first element via &v[0] or v.data():

cpp
std::vector<double> x, y;
// fill x and y
TGraph *g = new TGraph(x.size(), x.data(), y.data());

This combines the safety and flexibility of std::vector with the C-style interface many ROOT classes use internally.

Looping over vectors

In ROOT analysis you often apply the same operation to every element of a collection. This is common when summing energies, applying cuts, or filling histograms. Vectors are convenient for this, because you can easily loop over their elements.

The classic way is to use an index-based for loop. This works for both arrays and vectors:

cpp
std::vector<double> energies = {1.1, 2.3, 0.9, 4.2};
double sum = 0.0;
for (std::size_t i = 0; i < energies.size(); ++i) {
    sum += energies[i];
}

For a fixed-size array you use the known constant size:

cpp
double energy[4] = {1.1, 2.3, 0.9, 4.2};
double sum = 0.0;
for (int i = 0; i < 4; ++i) {
    sum += energy[i];
}

This style is useful when you need the index itself, for example if you want to fill a histogram with bin index or refer to corresponding elements in two arrays.

Another very readable option in modern C++ is the range-based for loop:

cpp
std::vector<double> energies = {1.1, 2.3, 0.9, 4.2};
double sum = 0.0;
for (double e : energies) {
    sum += e;
}

Here e is a copy of each element in turn. If you want to modify the elements stored in the vector, use a reference:

cpp
for (double &e : energies) {
    e = e * 1.05;  // apply a 5% scale factor
}

This pattern is common in physics analysis. For example, in a ROOT macro you might read a vector of hit energies and fill a histogram:

cpp
std::vector<double> hitEnergies;   // filled somewhere earlier
TH1D *hE = new TH1D("hE", "Hit energies", 100, 0.0, 10.0);
for (double e : hitEnergies) {
    hE->Fill(e);
}

When looping over vectors inside event loops, always remember that size() can be different for each event, especially if the vector stores per-event hits or tracks. Never assume a fixed length unless you explicitly enforce it.

In summary, C++ arrays are fixed-size and low-level, suitable when the size is known and interfaces require raw pointers. std::vector is dynamic, safer, and integrates well with ROOT workflows. You access and loop over both with indices starting at zero, and for vectors, range-based loops are a powerful and expressive way to process data in ROOT macros.

Views: 11

Comments

Please login to add a comment.

Don't have an account? Register now!