13.5. Reading a TTree
Table of Contents
Accessing branches
When you read data from a TTree you almost never work with the TTree alone. You ask for one or more of its branches and tell ROOT where in memory it should put the values it reads for each entry. For beginners it is enough to understand the following sequence: open the file, get the tree, set up access to branches, then iterate over entries.
A typical session starts by opening a ROOT file and retrieving the TTree object:
TFile *f = TFile::Open("data.root");
TTree *t = (TTree*)f->Get("myTree");At this point you probably want to know what branches the tree contains. The simplest inspection commands are:
t->Print(); // detailed structure, branches, types, leaves
t->Scan(); // quick table-like view of branch contents
The names printed by Print() are exactly the names you will use when you read data.
There are two common, beginner friendly ways to access branches: direct drawing expressions using TTree::Draw() and explicit branch access using addresses. The first way is covered in another chapter, so here the focus is on setting up reading through branch addresses, which gives you full control inside C++ code.
To read a branch you create a C++ variable of the appropriate type and ask the tree to fill it whenever you move to a new entry. This is usually done with SetBranchAddress() on the tree object:
int eventID;
float energy;
double time;
// Associate tree branches with C++ variables
t->SetBranchAddress("eventID", &eventID);
t->SetBranchAddress("energy", &energy);
t->SetBranchAddress("time", &time);
You must pass the address of your variable, so you use &variableName. When ROOT reads an entry it will write the branch value directly into that variable. The type of the variable must match the branch type. If the branch was written as Int_t you should use int. If it was Float_t use float, and for Double_t use double. If you are not sure, Print() will tell you the exact leaf type.
The C++ variable type must be compatible with the branch type, and you must pass its address to SetBranchAddress(). A wrong type, or forgetting the &, is a common source of crashes.
Strings and small fixed arrays appear very often in real data. A C-style character array branch can be read like this:
char detectorName[16]; // large enough buffer
t->SetBranchAddress("detName", detectorName);
Here you do not use &detectorName because an array already behaves like a pointer to its first element. For simple numeric fixed-size arrays, you typically declare a C array and pass it in the same way:
float hitEnergy[64];
t->SetBranchAddress("hitEnergy", hitEnergy);
Branches that hold std::vector<type> objects are common in modern ROOT files. They allow a different number of elements per event. Reading them uses a pointer:
std::vector<float> *hitE = nullptr;
t->SetBranchAddress("hitEnergyVec", &hitE);
ROOT will allocate and manage the std::vector objects as you call GetEntry(). Once the address is set you can use hitE in your event loop like any other pointer to a vector.
Sometimes you do not want to read all branches. Turning off unused branches is an important performance technique covered in a later chapter, but the basic mechanism is simple:
t->SetBranchStatus("*", 0); // disable all branches
t->SetBranchStatus("energy", 1); // enable only "energy"
t->SetBranchStatus("time", 1); // and "time"
With this configuration calls to GetEntry() will only read enabled branches into the variables you connected with SetBranchAddress(). This is especially useful for large TTrees and big files.
Looping over entries
Once your branches are connected to variables the central operation is an event loop. You go over all entries in the tree or a selected subset and for each entry you read the branch values and perform calculations or fill histograms.
The total number of entries can be obtained with GetEntries():
Long64_t nEntries = t->GetEntries();
The index of an entry runs from 0 to nEntries - 1. To read the contents of a specific entry you call GetEntry(entryIndex):
t->GetEntry(iEntry);
This call triggers ROOT to read the requested entry from disk and fill all the active branches into the variables you connected with SetBranchAddress(). After GetEntry() returns, your variables hold the values for that event and you can use them.
A minimal, complete event loop that reads all entries and prints something looks like this:
Long64_t nEntries = t->GetEntries();
for (Long64_t i = 0; i < nEntries; ++i) {
t->GetEntry(i); // load entry i into eventID, energy, time
std::cout << "Entry " << i
<< " eventID = " << eventID
<< " energy = " << energy
<< " time = " << time << std::endl;
}You usually replace printing with analysis operations such as filling histograms, computing derived quantities, or applying selection cuts. A typical pattern combines selection and histogram filling:
TH1F *hEnergy = new TH1F("hEnergy", "Energy;E [MeV];Events", 100, 0.0, 1000.0);
Long64_t nEntries = t->GetEntries();
for (Long64_t i = 0; i < nEntries; ++i) {
t->GetEntry(i);
// Example event selection
if (energy > 10.0 && time > 0.0) {
hEnergy->Fill(energy);
}
}Here the TTree provides input values, the event loop applies simple physics cuts, and the histogram collects the resulting distribution.
You are not forced to process every entry. You may want to inspect only the first few events or skip some entries. This is just standard C++ control over the loop:
for (Long64_t i = 0; i < nEntries; i += 2) { // process every second entry
t->GetEntry(i);
// analysis for this subset
}Or to limit processing to a maximum number of entries:
Long64_t maxEntries = std::min(nEntries, (Long64_t)10000);
for (Long64_t i = 0; i < maxEntries; ++i) {
t->GetEntry(i);
// quick test analysis on first 10k entries
}
When reading branches that hold std::vector objects the pattern inside the loop is slightly different, because each event can contain a different number of elements. After GetEntry() you query the size of the vector and loop over its entries:
Long64_t nEntries = t->GetEntries();
for (Long64_t i = 0; i < nEntries; ++i) {
t->GetEntry(i);
// hitE is a pointer to std::vector<float>
for (size_t j = 0; j < hitE->size(); ++j) {
float e = hitE->at(j);
// work with individual hit energy e
}
}This style is central in real analyses where each event may have a variable number of tracks, clusters, or detector hits.
It is often useful to check return values from GetEntry(), especially when you operate near the end of a tree or on corrupted data. GetEntry() returns the number of bytes read. If it returns zero or a negative value something is wrong or you are past the last valid entry:
if (t->GetEntry(i) <= 0) {
std::cerr << "Problem reading entry " << i << std::endl;
break;
}
A correct event loop always calls GetEntry() for each entry index you want to process. Using branch variables without a preceding GetEntry() means you are working with stale values from an old entry.
By combining careful branch access and a well structured loop over entries you gain full programmatic control of TTree contents. This is the foundation for all later topics, such as drawing directly from TTrees, applying selection cuts, or using higher level interfaces like RDataFrame.
Views: 8
KAHIBARO