C++ in a Nutshell: all you need to know to use ROOT
Welcome to the Monday afternoon hands-on session. We will focus today on introductory exercises about the C++ language.
After the theoretical introduction you followed, you should be able to solve the exercises. Nevertheless two levels of help will be provided: a hint and a full solution. Try not to jump to the solution even if you experience some frustration.
The help is organised as follow:
Here the solution is shown.
Some points linked to the exercises are optional and marked with a
icon: they are useful to scrutinise inmore detail some aspects of C++. Try to solve them if you have the time.
C++ is a compiled language and in order to obtain executables the procedure would imply the usage of a compiler. We will take advantage of a powerful feature of
ROOT, which allows to compile C++ code without bothering about all the complications linked to the direct usage of the compiler.
Here follows a reminder about the way in which you can compile macros from within the cint interpreter.
Compiling macros with ROOT
Suppose you would like to compile this trivial macro:
void test(){ int a = 42;}
you would have to execute this command:
root [0] .L myMacro.cxx+
don't worry about the warning for now. To execute your code you then should type:
root [1] test()
The macros of the exercises together with the solutions can be found here:
cppTutoriaROOT.tar.gz:
Exercise 1: Syntax error
The compilation of the C++ code is a way in which quite an amount of mistakes can be intercepted. Among the most common we can find the so-called "syntax error", i.e. an error in the way in which the language is used.
Take for example the macro
syntaxError.cxx
(reported here) and compile it.
Can you inspect the output to spot the mistake and correct it?
What is the compiler suggesting about line 9?
Just add a ;
after the float result = n1+n2
line.
Exercise 2: Hello world!
Now that you know how to interpret the suggestions that the compiler can give you, you could try to write down a simple program that prints on screen out the string
"Hello World"
. Try to store the string in a variable and then print it.
The building blocks you would need are:
-
cout
: the name of the stream linked to the display of the machine. The operator to "feed" cout is the <<
.
- The carriage return can be part of the string coded as a
\n
or expressed as endl
.
- The C++ type for strings is the
string
while you can express string constants enclosed between double quotes (e.g. "My String").
#include <iostream>
using namespace std;
void helloWorld(){
string helloWorld ("This is an Hello World program!");
cout << helloWorld << endl;
}
Can you try out the operator
+
with different strings? What would you expect from it? What happens using operator
*
?
Exercise 3: while loops and conditional statements
In this exercise we will put in place a while loop and use conditional statements. The goal is simple: sum up all numbers which are an exact multiple of three until 1000 is reached. How many of them are needed? Which ones?
These are some suggestions:
- This is a use-case for an uncountable loop steered by the
while
control structure
#include <iostream>
using namespace std;
void sumMultiples() {
unsigned int index=0;
unsigned int addedNumberIndex=0;
unsigned int sum = 0;
while (sum<=1000){
if (index%3 == 0){
addedNumberIndex++;
std::cout << addedNumberIndex << ") "<< index << " being added to the sum.\n";
sum+=index;
}
index++;
}
}
In this particular case, it is possible to use a
for
instead of a
while
. How? How do you judge the clarity of the two solutions?
Exercise 4: Mean and RMS calculation
This exercise is dedicated to the codification of an algorithm to calculate mean and RMS starting from a set of numbers.
The macro has been started for you in the file
forLoop.cpp
. The idea is that you complete the code with what is missing to calculate and print the mean and RMS of the numbers collected in the vector.
The building blocks you would need are:
- One way in which you can loop over a container like the vector is to use a running integer index in a
for
loop
- You can access the elements of the vector using the operator
[]
#include <iostream>
#include <vector>
using namespace std;
void calculateMomenta () {
unsigned int N=8;
vector<float> myvector;
myvector.reserve(N);
// fill the vector
myvector.push_back(1.2f);
myvector.push_back(11.2f);
myvector.push_back(1.32f);
myvector.push_back(0.2f);
myvector.push_back(1.906f);
myvector.push_back(3.f);
myvector.push_back(32.f);
myvector.push_back(13.f);
float sum=0;
for (unsigned int index=0;index<N;++index){
sum+=myvector[index];
}
float mean = sum/N;
cout << "The mean is: " << mean << endl;
float RMS2=0;
for (unsigned int index=0;index<N;++index){
float element=myvector[index];
float residual = element - mean;
RMS2+=residual*residual;
}
RMS2/=(N-1);
float RMS=sqrt(RMS2);
cout << "The RMS is: " << RMS << endl;
}
Can you calculate the mean and the RMS in one single loop?
Exercise 5: Functions
One powerful feature of the C++ language is the possibility to use functions. Actually, we used functions in each exercise up to now and called them from within the interpreter. This exercise will focus on the writing of a function that calculates the mean and the rms of a given vector in order for the caller to print these two values on screen. You can start from the code you developed for exercise 4.
The building blocks you would need are:
- By default, C++ passes arguments to functions by value. It means that the parameter is "cloned" within the scope of the function starting from the one passed by the caller. It is therefore pointless to operate changes on "clones" as they will disappear as soon as the function terminates its execution.
- A possible treatment of the parameters given the aforementioned behaviour is to pass values by reference (the signature of the function should be
type functionName(type& parameterName)
)
#include <iostream>
#include <vector>
using namespace std;
void calculateMomenta (vector<float>& myvector, float& mean, float& rms) {
const unsigned int N = myvector.size();
float sum=0;
for (unsigned int index=0;index<N;++index){
sum+=myvector[index];
}
mean = sum/N;
float rms2=0;
for (unsigned int index=0;index<N;++index){
float element=myvector[index];
float residual = element - mean;
rms2+=residual*residual;
}
rms2/=(N-1);
rms=sqrt(rms2);
}
//---------------------
void functions(){
unsigned int N=8;
float thisMean, thisRMS;
vector<float> myvector;
myvector.reserve(N);
calculateMomenta(myvector, thisMean, thisRMS);
std::cout << "Mean: " << thisMean << " -- RMS: " << thisRMS << endl;
// fill the vector
myvector.push_back(1.2f);
myvector.push_back(11.2f);
myvector.push_back(1.32f);
myvector.push_back(0.2f);
calculateMomenta(myvector, thisMean, thisRMS);
std::cout << "Mean: " << thisMean << " -- RMS: " << thisRMS << endl;
myvector.push_back(1.906f);
myvector.push_back(3.f);
myvector.push_back(32.f);
myvector.push_back(13.f);
calculateMomenta(myvector, thisMean, thisRMS);
std::cout << "Mean: " << thisMean << " -- RMS: " << thisRMS << endl;
}
Could you list the principal advantage in the usage of functions?
A
nan value is printed for the mean in the first case. The acronym
nan stands for "Not A Number". Can you explain what is happening?
Exercise 6: Putting all together with classes
Imagine now an entity that
describes the ensamble of the vector of values and the functions for calculating their mean and RMS: that would be a class. The entity
described by this class would be something concretely holding the data and would be allowed to be present in multiple instances: these would be objects.
You can find the class in the
measurementsClass.cxx
file. The goal of this exercise is to create an object of the type
Measurements
, fill it with the values written below and print out on screen the mean and rms of these values using the methods of the class.
This last exercise acts as a final preparation step towards the usage of the
ROOT classes.
Values with which the Measurements object should be filled:
- -7.31
- 6.95
- 5.28
- -4.90
- -0.09
- -1.01
- 3.03
- 5.77
- -8.12
- -9.43
These are some suggestions:
- The method to add a value to the object is
addValue
while in order to get mean and rms, they are getMean
and getRMS
respectively
for (unsigned int index=0;index<N;++index){
sum+=m_values[index];
}
return sum/N;
};
float getRms(){
const unsigned int N = m_values.size();
float rms2=0;
for (unsigned int index=0;index<N;++index){
float element=m_values[index];
float residual = element - getMean();
rms2+=residual*residual;
}
return sqrt(rms2/(N-1));
};
private:
std::vector<float> m_values;
};
int main(){
Measurements myMeasurements;
myMeasurements.addValue(-7.31);
myMeasurements.addValue(6.95);
myMeasurements.addValue(5.28);
myMeasurements.addValue(-4.90);
myMeasurements.addValue(-0.09);
myMeasurements.addValue(-1.01);
myMeasurements.addValue(3.03);
myMeasurements.addValue(5.77);
myMeasurements.addValue(-8.12);
myMeasurements.addValue(-9.43);
std::cout << "Mean: " << myMeasurements.getMean() << ", RMS: " << myMeasurements.getRms() << std::endl;
return 0;
}
The evaluation of mean and rms is less than optimal. For every call to them, calculations are started even if no new value has been added. Can you imagine a caching mechanism? A good way to start could be to add two private
float
members m_mean_cache and m_rms_cache together with a boolean flag, always private, which is used to track if a new value has been added to the collection since the last calculation of mean and rms took place. Then one should implement a simple
if
condition to select between a new calculation of mean and rms or to return the cached values.
Note: for performance reasons beyond the scope of these exercises, the calls to the
push_back
method of the standard vector should be always preceded by a call to the
reserve()
method. In this context, we will not go into this level of detail.