2.6 Functions
Table of Contents
Defining functions
Functions in C++ group a sequence of statements into a reusable block. In ROOT analysis you will often wrap parts of your code into functions so that you can call them many times without repeating the same lines.
A function has four essential parts: a return type, a name, a list of parameters in parentheses, and a body in curly braces. A very simple function that prints a message could look like this in a ROOT macro:
void Hello() {
std::cout << "Hello from ROOT!" << std::endl;
}
Here, void is the return type, Hello is the function name, the empty parentheses mean it takes no arguments, and the body is everything inside {}.
In a .C macro file you typically place function definitions at file scope, not inside other functions. ROOT’s C++ interpreter can then see them and you can call them from the ROOT prompt or from other functions in the same file. For example, you could put the definition of Hello in hello.C, start ROOT, and then type:
root [0] .L hello.C
root [1] Hello()
Hello from ROOT!A function body can contain any valid C++ statements: variable declarations, loops, conditionals, calls to ROOT classes, and so on. For example, a function that creates and draws a small histogram might look like:
void SmallHist() {
TH1F h("h", "Example histogram", 50, 0.0, 5.0);
h.Fill(1.0);
h.Fill(2.3);
h.Draw();
}
This defines a new function called SmallHist that you can call from ROOT to quickly create and display this histogram.
You can also forward declare functions before you define them. A declaration tells the compiler that a function exists and what its signature is, without giving the body. In simple ROOT macros you can often avoid separate declarations by putting your full function definitions before they are first used, that is, define helper functions above void main()-style entry functions such as void myAnalysis().
A function definition must specify the return type, the name, the full parameter list with types, and the body in {}. In a ROOT macro, define functions at file scope, above the code that calls them, to avoid missing-declaration problems.
Function arguments
Most useful functions need input values to work with. These inputs are called arguments, and in the function definition they are called parameters. Each parameter has a type and a name. When you call the function, you provide values, and those values are copied into the parameters.
Here is an example of a function that computes the area of a rectangle given its width and height:
double RectangleArea(double width, double height) {
double area = width * height;
return area;
}
In this example, width and height are parameters of type double. When you call this function, you pass two numbers:
double a = RectangleArea(3.5, 2.0); // width = 3.5, height = 2.0
The order of arguments matters. The first argument goes into the first parameter, the second into the second parameter, and so on. Types must be compatible with the parameter types. If you define a function that expects int and you pass a double, an implicit conversion may occur, which can lose information.
You can use different types for different parameters in one function. For example, in analysis code you might write:
void FillWithConstant(TH1F &hist, float value, int nTimes) {
for (int i = 0; i < nTimes; ++i) {
hist.Fill(value);
}
}
Here the function takes a histogram by reference, a floating point value, and an integer number of times. The & means the function can modify the original histogram object, not a copy. This is very common in ROOT when you want a function to fill or style objects that you created elsewhere.
C++ also supports default argument values in function declarations, which means you can omit some arguments when calling the function. For simple ROOT macros you might see:
void DrawGaussian(int nEvents = 1000) {
TH1F h("h", "Gaussian", 100, -5, 5);
for (int i = 0; i < nEvents; ++i) {
h.Fill(gRandom->Gaus(0, 1));
}
h.Draw();
}
You can call DrawGaussian() with no arguments, and nEvents will be 1000, or you can call DrawGaussian(5000) to override the default.
To summarize the basic rules, consider the following table:
| Concept | Example | Effect |
|---|---|---|
| Parameter type | double x | Function expects a double input |
| Multiple parameters | double f(int n, float x) | Function takes two inputs of different types |
| Reference parameter | TH1F &h | Function can modify the original histogram |
| Default argument | int nEvents = 1000 | Function can be called with or without this argument |
Function parameters must include both type and name, and the order and types of arguments in calls must match the parameter list. Use reference parameters like TH1F &h when you want to modify ROOT objects passed into your function.
Return values
The return value is the result that a function gives back to the caller. The type of this result is written before the function name in the definition. Inside the function body, the return statement provides the actual value.
We already saw a simple example:
double RectangleArea(double width, double height) {
double area = width * height;
return area;
}
Here, double is the return type, and the function returns the variable area. When you call this function, you can store its return value in another double:
double a = RectangleArea(3.5, 2.0);The type of the variable that receives the return value must be compatible with the function’s return type.
If a function should not return a value, you use the special return type void. In that case you usually omit return, except if you want to exit the function early:
void PrintIfPositive(double x) {
if (x <= 0) {
return; // exit without printing
}
std::cout << "x is positive: " << x << std::endl;
}Many ROOT-related functions return numbers that characterize objects, such as the mean of a histogram, or return pointers to created objects. For example:
TH1F* MakeEmptyHist(const char* name, const char* title,
int nBins, double xMin, double xMax) {
TH1F *h = new TH1F(name, title, nBins, xMin, xMax);
return h;
}
Here the return type is TH1F*, a pointer to a histogram. The function allocates a histogram and returns its pointer. The caller can then do:
TH1F *myHist = MakeEmptyHist("h1", "My hist", 100, 0.0, 10.0);
myHist->Draw();In this pattern you must understand ROOT’s object ownership rules, which are discussed in a later chapter, to avoid memory and lifetime problems.
You can also return bool to signal success or failure of an operation:
bool IsInRange(double x, double low, double high) {
return (x >= low) && (x <= high);
}
This function returns either true or false, which is very useful in selection logic for analysis.
For numerical functions, it is often helpful to document, in comments, what the return value represents and in which units. This is especially important in physics analysis to avoid mixing centimeters with millimeters or MeV with GeV.
The return type at the start of a function must match the type of the value supplied in return. Functions with non-void return types must return a value on every possible execution path. Use void for functions that only perform actions, and non-void types when you need a result to use later in your ROOT analysis.
Views: 12
KAHIBARO