KAHIBARO
Discord Login Register

3.1 Classes and Objects

Class definitions

In Geant4 you will work with many C++ classes, both from the Geant4 library and your own user classes. Understanding how to define a simple class is essential, because all Geant4 user components, such as detector construction or primary generator, are implemented as C++ classes.

A C++ class is a user defined type that groups data and the functions that operate on that data. The data are called data members or member variables, and the functions are called member functions or methods. A very simple class definition looks like this:

cpp
// MyCounter.hh
#ifndef MYCOUNTER_HH
#define MYCOUNTER_HH
class MyCounter {
  public:
    void SetValue(int value);
    int  GetValue() const;
  private:
    int fValue;
};
#endif

The keyword class introduces the class name. The body of the class is enclosed in braces and ends with a semicolon. The public section lists the interface that other parts of the program can use. The private section contains implementation details that are hidden from outside code. Geant4 uses this same pattern for almost all of its classes, for example G4VUserDetectorConstruction or G4RunManager.

Inside the class, functions are only declared. Their implementation goes into a source file:

cpp
// MyCounter.cc
#include "MyCounter.hh"
void MyCounter::SetValue(int value) {
  fValue = value;
}
int MyCounter::GetValue() const {
  return fValue;
}

The MyCounter:: prefix tells the compiler that these functions belong to the MyCounter class. Geant4 user classes are structured in the same way: the class is declared in a header (.hh) file and defined in a source (.cc) file. You will subclass many Geant4 base classes, such as G4VUserActionInitialization, using this basic template.

In Geant4 projects always put class declarations in header files and the function definitions in source files, and remember the semicolon after the class declaration.

Creating objects

Once a class is defined, you can create objects, which are concrete instances of that class. Each object has its own copy of the data members, but shares the same member functions.

The simplest way to create an object is as a local variable on the stack:

cpp
MyCounter counter;          // create an object
counter.SetValue(10);       // call a member function
int v = counter.GetValue(); // read back the value

This object is automatically created when the line is executed and automatically destroyed when it goes out of scope, for example when the function returns. This style is used frequently in Geant4 for short lived helper objects.

Often in Geant4 you will need objects that live longer and are passed to the Geant4 kernel, such as your detector construction or physics list. These are usually created dynamically using new:

cpp
MyDetectorConstruction* det = new MyDetectorConstruction();
// pass to Geant4, for example
runManager->SetUserInitialization(det);

Here, det is a pointer to an object on the heap. Geant4 will keep this pointer and use your object throughout the run. The lifetime of such objects is typically managed by Geant4 itself or by your main program, depending on the class and context. You should not use delete on objects that Geant4 takes ownership of unless the manual explicitly says so.

Geant4 also creates many internal objects and gives you pointers to them, such as G4Track or G4Step in user actions. You do not create or destroy these. You only use them when Geant4 passes them to your user functions.

Only delete objects that you created with new and that you still own. Never call delete on pointers given to you by Geant4 unless the documentation clearly states that you must manage their lifetime.

Constructors

A constructor is a special member function that is called automatically when an object is created. It has the same name as the class and no return type. You use constructors to initialize your member variables and to set up the internal state of the object.

Here is MyCounter with a constructor:

cpp
// MyCounter.hh
class MyCounter {
  public:
    MyCounter();           // constructor
    void SetValue(int value);
    int  GetValue() const;
  private:
    int fValue;
};

And its implementation:

cpp
// MyCounter.cc
#include "MyCounter.hh"
MyCounter::MyCounter()
: fValue(0)        // member initializer list
{
  // optional body
}
void MyCounter::SetValue(int value) { fValue = value; }
int  MyCounter::GetValue() const    { return fValue;  }

The member initializer list : fValue(0) is called before the body of the constructor. It is the preferred way to initialize member variables, and you will see this style in almost all Geant4 examples. It is especially important when your class has members that are themselves classes, for example G4Material or G4LogicalVolume.

Classes can have several constructors with different parameter lists. This is called overloading and lets you create objects in different ways:

cpp
class MyCounter {
  public:
    MyCounter();              // default constructor
    MyCounter(int initial);   // constructor with argument
  private:
    int fValue;
};

In Geant4, user classes frequently use constructors to receive configuration from the main program:

cpp
MyDetectorConstruction::MyDetectorConstruction(G4double size)
: G4VUserDetectorConstruction()
, fWorldSize(size)
{}

Here, MyDetectorConstruction forwards parameters, such as geometry dimensions, into member variables, and also calls the base class constructor. This pattern appears in most user classes that derive from Geant4 base classes.

Always initialize all member variables, preferably in the constructor initializer list. Uninitialized members can cause crashes or incorrect results that are very hard to debug.

Destructors

A destructor is a special member function that is called automatically when an object is destroyed. It has the same name as the class, but with a ~ prefix, and no return type or parameters:

cpp
class MyCounter {
  public:
    MyCounter();
    ~MyCounter();     // destructor
    void SetValue(int value);
    int  GetValue() const;
  private:
    int fValue;
};

Its implementation might look like this:

cpp
MyCounter::~MyCounter() {
  // clean up resources if needed
}

When an object goes out of scope, or when you call delete on a pointer created with new, the destructor runs. The destructor is responsible for cleaning up any resources the object owns. This can include memory allocated with new, files, or other external resources. For simple classes that only contain built in types, the destructor can be empty.

In Geant4, many user classes define a destructor even if it is empty, for clarity:

cpp
MyDetectorConstruction::~MyDetectorConstruction() {
  // often nothing to do, Geant4 manages most resources
}

You rarely need to delete Geant4 objects that you did not create, and in many cases the Geant4 kernel will delete user initialization classes at the correct time. However, if your class allocates its own dynamic memory, you must free it in the destructor:

cpp
class MyDataHolder {
  public:
    MyDataHolder();
    ~MyDataHolder();
  private:
    G4double* fArray;
};
MyDataHolder::MyDataHolder()
: fArray(new G4double[100])
{}
MyDataHolder::~MyDataHolder() {
  delete [] fArray;
}

In modern C++, and also in Geant4 code, it is common to avoid manual memory management by using objects that manage their own resources. Whenever possible, prefer standard containers such as std::vector over raw pointers, so that there is nothing special to do in the destructor.

Every new in your own class should have a corresponding delete in the destructor, unless ownership is intentionally transferred elsewhere. Missing delete calls cause memory leaks that become serious in long Geant4 runs.

Views: 10

Comments

Please login to add a comment.

Don't have an account? Register now!