KAHIBARO
Discord Login Register

20.2. Storing std::vector Objects

Variable-length event data

In many real analyses each event does not contain the same fixed number of measurements. One event may have 2 tracks, another 15, another 0. Fixed-size C arrays in TTrees are inconvenient in this situation, because you either need a separate “number of elements” branch, or you have to guess a maximum size and leave many unused entries. ROOT can instead store C++ std::vector objects directly in branches, which is much more natural for variable-length data.

The basic pattern is that one branch holds a whole std::vector<T> for each entry. The length of the vector can change from event to event. ROOT will write and read the full contents of the vector transparently. Typical examples in particle and nuclear physics include a vector of hit energies in a detector, a vector of track transverse momenta, or a vector of times in a trigger window.

To store a vector in a TTree, you usually follow these steps in your macro or program:

  1. Create the vector object on the heap, for example with new.
  2. Create a TTree.
  3. Create a branch that points to the vector pointer.
  4. For each event, fill or modify the vector, call Fill(), then clear or resize the vector for the next event.

A minimal example for a vector of floating point values could look like this (error checking omitted for clarity):

cpp
TFile *f = new TFile("vectors.root", "RECREATE");
TTree *t = new TTree("t", "Tree with std::vector");
std::vector<float> *hits = new std::vector<float>;
t->Branch("hits", &hits);
for (int ievt = 0; ievt < 100; ++ievt) {
    hits->clear();
    int nhit = gRandom->Integer(10);   // different number of hits each event
    for (int i = 0; i < nhit; ++i) {
        float e = gRandom->Gaus(5.0, 1.0);
        hits->push_back(e);
    }
    t->Fill();
}
f->Write();
f->Close();

In this pattern the object pointed to by hits stays the same for all events, but its size and contents change every event. ROOT uses its I/O system to serialize and store the full vector for each entry.

ROOT must know the dictionary for the template type of the vector at compile time. For common simple types such as std::vector<int>, std::vector<float>, std::vector<double> and many standard STL containers, modern ROOT builds already ship the necessary dictionaries. For user defined classes inside vectors, you must ensure that a dictionary exists for both the class and the std::vector<YourClass> template, typically by using ROOT’s dictionary generation system. Otherwise the tree cannot be written or read correctly.

When storing std::vector objects in TTrees, always keep the vector itself alive for the whole lifetime of the TTree and reuse it for all entries. Do not delete and reallocate the vector between Fill() calls, and do not use a local (stack) vector that goes out of scope while the tree still needs it.

Because the vectors store variable-length data, the storage per event is no longer constant, so you should pay a bit more attention to performance for very large datasets. Compression settings, splitting, and branch-level I/O tuning can become important and are covered in more specialized documentation, but the fundamental usage pattern is the same as in the simple example.

Reading vector branches

Once a TTree that contains std::vector branches is stored in a file, reading the data looks very similar to reading simple types, with a few important details about pointer types and object ownership.

A typical manual reading pattern is:

  1. Open the ROOT file and get the TTree.
  2. Create a pointer to a std::vector<T> and initialize it to nullptr.
  3. Call SetBranchAddress with the name of the branch and the address of the vector pointer.
  4. Loop over the entries, call GetEntry, and use the contents of the vector in each event.

For example, to read the hits branch created in the previous section:

cpp
TFile *f = TFile::Open("vectors.root");
TTree *t = (TTree*)f->Get("t");
std::vector<float> *hits = nullptr;
t->SetBranchAddress("hits", &hits);
Long64_t nentries = t->GetEntries();
for (Long64_t i = 0; i < nentries; ++i) {
    t->GetEntry(i);
    int nhit = hits->size();
    for (int j = 0; j < nhit; ++j) {
        float e = hits->at(j);
        // use e here, for example fill a histogram
    }
}

Here SetBranchAddress takes the address of the std::vector<float>* pointer, so that ROOT can allocate and manage the actual std::vector<float> object and update the pointer as entries are read. You should not new the vector yourself in this pattern. ROOT will create and reuse the underlying vector as needed.

You can also use the subscript operator instead of at() when you are confident that the index is in range. For performance critical code this is common:

cpp
for (size_t j = 0; j < hits->size(); ++j) {
    float e = (*hits)[j];
    // process e
}

Manual loops are useful when you want full control over the analysis. However, for quick studies ROOT offers shortcuts. One convenient option is to use TTree::Draw directly on vector branches, for example plotting the distribution of all hit energies from the hits branch:

cpp
t->Draw("hits");

In this call ROOT automatically iterates over entries and over the components of each vector and produces a one dimensional histogram of the individual elements. You can also apply selection cuts that depend on the vector contents, but the syntax can be subtle and is covered in more advanced material.

With modern ROOT you can also access vector branches very naturally using RDataFrame. In that case you do not set branch addresses manually. Instead, you create an RDataFrame from the tree and use the branch name directly as a column. If the branch is a std::vector<T>, the column has C++ type std::vector<T>, so in Define or Filter expressions you can use methods like .size() or index operators to inspect and transform the data.

When reading vector branches with SetBranchAddress, always pass the address of a pointer of the correct type, for example std::vector<double>*. Do not pass the address of a local std::vector<double> object, and do not mix types across reading and writing. A branch written as std::vector<float> must be read as std::vector<float>, not as std::vector<double>.

Once you are comfortable with this pattern you can extend it to more complex event structures, such as multiple vector branches that all have the same event length or combinations of scalar and vector branches. This enables highly flexible storage of event data where each event carries exactly as much information as it needs, without artificial fixed-size limits.

Views: 11

Comments

Please login to add a comment.

Don't have an account? Register now!