How to Write Objects to a File?

All instances of ROOT classes inheriting from TObject can be written to a ROOT file. This can be achieved using the TObject::Write method. Let's see an example:

In [1]:
TFile outputFile ("outputFile.root","RECREATE");
TH1F h ("myHisto","Histogram Title",100, -2, 2);
h.Write();

What happens behind the scenes when calling the TH1F::Write method, in a nutshell, is the following:

  • A buffer object is created (see class TBuffer).
  • The buffer is filled by invoking obj->Streamer(b) (see below).
  • The buffer is written to the file.
    • ROOT knows in which file it should write since after opening the TFile a global variable, gDirectory, points to the file.
    • If the file compression attribute is set (TFile::SetCompressionLevel), the buffer holding an object is compressed before being written to the file.
  • A new key with the name "myHisto" is added to the list of keys of the TFile. Objects in a file are identified by a key (see class TKey). A key is itself an object with all the necessary information to locate an user object in a file (its name, a title, size, position and a few other parameters). A file has a directory consisting of the list of keys.

The Streamer method is responsible for scanning the object data structure and serialize the data to the buffer. This function may be user-written, but is, in general, automatically created when generating a dictionary. To illustrate how a Streamer is working, we will take the case of the ROOT class TShape. A TShape object describes a detector geometry basic element. TShape derives from TNamed that derives from TObject. It also derives from two ROOT attribute classes TAttLine and TAttFill. The TShape object has a few data members that are basic types and also a pointer to a material object. Streamer includes the code to read and write an object. We concentrate for the time being only on the writing part.

In [ ]:
void TShape::Streamer(TBuffer &b)
{
   // Stream a TShape object
 
   if (b.IsReading()) {
      Version_t v = b.ReadVersion();
      TNamed::Streamer(b);
      TAttLine::Streamer(b);
      TAttFill::Streamer(b);
      b >> fNumber;
      b >> fVisibility;
      b >> fMaterial;
   } else {
      b.WriteVersion(TShape::IsA());
      TNamed::Streamer(b);
      TAttLine::Streamer(b);
      TAttFill::Streamer(b);
      b << fNumber;
      b << fVisibility;
      b << fMaterial;
   }
}

A statement like b << fNumber means: encode data member with name fNumber into buffer b. The TBuffer class defines the operators << and >> for all basic data types, but also for pointers to objects. Let's now have a look to what b << fMaterial is doing. fMaterial is a pointer to the material object. However, many shapes may reference the same material and we may want to write in the same buffer the complete list of all shapes, still writing only one copy of the referenced material. This is done automatically by ROOT. TBuffer keeps a table of pointers to the objects already written in the current buffer. When a reference to an already saved object is encountered, only the serial number of the object in the table is written, not the object itself.

The statement TNamed::Streamer invokes the Streamer function of the TNamed class responsible for saving its own data members. TNamed, in turn, will call TObject::Streamer. Note that the first operation is to write the class version identifier of the TShape class. This consists of two bytes and can be set by the user either in the code of the class or in the selection file used to obtain the class dictionary. In this case it corresponds to the Class version ID given in the ClassDef statement in the TShape include file. This version ID can be used in the reading part to take into account possible (and likely) changes in the class definition. Therefore ROOT has an overhead of two bytes per level of inheritance for each object. This is necessary to support a complete and safe schema evolution. A mechanism with one single identifier per object would not be sufficient.

In the large majority of cases, the Streamer method generated in the dictionary is appropriate. You can implement your own Streamer method if you need to perform special operations related to the serialisation of the object.

A small note: when your object derives from TNamed, you may omit the parameter keyname. For example, for a TShape object, one could write: obj->Write(). The key created in this case will get the name from the TNamed object. If you write a key with a name already existing in the file, a new cycle is created. You can use TFile::ls or TFile::Map to see the list of all records in a file.

In [2]:
outputFile.ls()
TFile**		outputFile.root	
 TFile*		outputFile.root	
  OBJ: TH1F	myHisto	Histogram Title : 0 at: 0x7f9599cf9338
  KEY: TH1F	myHisto;1	Histogram Title

In general, all instances of classes that have a dictionary can be written by ROOT in ROOT files, for example:

In [2]:
std::vector<int> v {1,2,3,4};
outputFile.WriteObjectAny(&v, "std::vector<int>","myVector");
outputFile.ls();
outputFile.Close();
TFile**		outputFile.root	
 TFile*		outputFile.root	
  OBJ: TH1F	myHisto	Histogram Title : 0 at: 0x7f652e23c338
  KEY: TH1F	myHisto;1	Histogram Title
  KEY: vector<int>	myVector;1	object title