Modularizing pp6calculator and Automating its Build With Ben Morgan Developer Workflow Build and Test git commit $ g++ pp6calculator.cpp -o pp6calculator $ ./pp6calculator “Add CMake build” Edit Sources Add Files Building a C++ based Executable Math.hpp Math.o Math.cpp Monolith.cpp MLMain.cpp Monolith.exe MLMain.o Physics.cpp Physics.o Physics.hpp Defeat in Detail - Modularize and Automate «Math.hpp» «Math.cpp» //! /file Math.hpp // "Header Guard" // We can only have one declaration // of any signature, so we use the // preprocessor to conditionally // include the code block between the // #ifndef/#endif blocks once only // #ifndef PP6CALCULATOR_MATH_HPP #define PP6CALCULATOR_MATH_HPP // - Math.cpp : Implementation // #include the header so declarations // are matched #include “Math.hpp” //! The add function signature - its `declaration` int add(double a, double b, double& answer); // Implementation of add - its `definition` int add(double a, double b, double& answer) { answer = a + b; return 0; } // Further definitions can follow... // Further declarations can follow... #endif // PP6CALCULATOR_MATH_HPP Modularization - Headers and Sources $ g++ pp6calculator.cpp -o pp6calculator $ ./pp6calculator $ git add pp6calculator.cpp $ git commit -m “A change” $ vim pp6calculator.cpp $ g++ pp6calculator.cpp -o pp6calculator $ ./pp6calculator $ git add pp6calculator.cpp What about -m “More changes” $ git commit diļ¬erent compilers $ vim pp6calculator.cpp and/or flags? $ g++ pp6calculator.cpp -o pp6calculator What happens $ ./pp6calculator when we begin $ git add pp6calculator.cpp modularizing? $ git commit -m “Yet more changes” $ vim pp6calculator.cpp ... Is it really so awkward? What about other users? What about system specific features? «configure» Find needed tools Adapt code for system Generate build task «build» Compile code Link binary Test binary Build Automation «install» Install binary on system Create installer for other systems GNU Make/Autotools Qt qmake Tools for Build Automation pp6calculator.git README.md pp6calculator.cpp CMakeLists.txt Makefile pp6calculator.xcodeproj CMake - A “Metabuild” System pp6calculator.sln pp6calculator.git README.md pp6calculator.cpp CMakeLists.txt pp6calculator.DBG pp6calculator.build $ cmake \ ../ppcalculator.build Makefile CMakeCache.txt cmake_install.cmake CMakeFiles/ $ cmake -DDEBUG=ON \ ../ppcalculator.build CMake Workflow - Separation of Source and Build Modularization and CMake-ification of pp6calculator • In the following walkthrough, we’ll modularize pp6calculator into a main application plus units of functionality, introducing software libraries. We’ll see how this benefits the ease of use and maintenance of the code, but also how it makes building pp6calculator more awkward. We’ll bring in CMake to help automate this and integrate it to our workflow to make it more streamlined and robust. Finally, we’ll see how we can package pp6calculator for use by others. Of course, we’ll continue to tracks changes using git! • Modularize pp6calculator into a main executable and a support library • CMake syntax and workflow for building/linking executables and libraries • Use of CPack to generate source and binary packages Tools you’ll need Walkthrough From Scratch • If you’re still working on your code from this morning and don’t want to disrupt its development, you can instead clone our repository from github and use that as a starting point. • To do this, use the following commands to clone it and create a backup (NB, this should all be done outside your working copy!!) $ cd $HOME $ mkdir mpagspp6-upstream && cd mpag6pp6-upstream $ git clone git://github.com/drbenmorgan/pp6calculator.git \ pp6calculator_dbm.git $ cd pp6calculator_dbm.git $ git checkout -b mysolutions $ git branch • You’ll working on the “mysolution” branch. The “master” branch will follow my solutions. We’ll see how to compare them later. 1: Recap on Project Structure Remember how on Day 1 we recommended creating a clean directory in which to further create a subdirectory for your git repository to reside? My setup and a tree of the structure is shown below left. The reasons for this layout will become clear today, so if you don’t have this arrangement, now is the time to create it! In the worst case, simply commit all your local changes, push them to github and finally clone the github repository into its new location. Notes Once you start to manage many working copies of projects, you may want to create a dedicated directory in your home directory to hold these. You may also wish to further partition using Reverse-DNS directories, e.g. ch.cern/ for CERN work, com.github/ for your projects. 2: How Many Lines? If you look a the current implementation of pp6calculator, it’s starting to get a little unwieldy. It’s 406 lines long, with all the functions at the top of the file, and the main program at the bottom. That’s not too bad in the big scheme of things, but we’d like to partition the functionality a bit better as we know we’re going to add new features as we go through the course. We’re going to start by looking at function declarations (or prototypes) Notes You will find code which has source files composed of thousands of lines! Studies of human cognition suggest that our working memory can hold “7±2” things at once. This can be a useful guide as to how long a typical function or scope block should be. 3: Function Declarations Before we can use a function in C++, it must be known to the compiler. You might have seen this already when using std::cout if you forgot to include the iostream header. In your pp6calculator program, move all of the math/array functions you’ve implemented to the end of the .cpp file so that the main function is the first that appears in the file. Try recompiling - does it work? Notes If you’ve begun modularizing the code already, then you can skip this step, but be aware of where the function declarations you use appear to the compiler when they’re #included from another file. 4: No Declaration, No Go You’ll have found the compilation fails with many “not declared in this scope errors”. As you’ll have guessed from earlier discussion, this is due to the use of a function that is unknown to the compiler at the point the use is encountered. We could fix this by moving the functions back to before main, but instead we’ll declare them before main, and leave the definitions(implementation) where it it. Notes We’ve used the terms function declaration and function prototype interchangeably here. For C ++ this is o.k., but in older style C there is a difference! Here, you can declare a function without specifying the types of the arguments. A prototype is a declaration that includes the number and type of its arguments. 5: Declaring Functions To declare a function, we simply specify its return type, name and the number and types of the arguments it takes, omitting the actual body, i.e. implementation, of the function. The declaration tells the compiler about the function interface, and promises that its implementation will be found “somewhere else”. Add declarations for your functions at the beginning of your main program, using my example for the add function of pp6calculator in the left hand tile below. Check that you can now compile and run the program o.k. Notes Function declarations are our first concrete use of an interface. The key point to grasp is that an interface frees us from worrying how a task is done, just that it is done. The implementation might be intellectually interesting, but knowledge of it is not needed to use the interface. 6: Handling Link Errors One issue you might see with separate declarations and definitions are errors occurring at link time, i.e. you have syntactically correct code, but something goes wrong when the linker tries to produce the final executable. Comment out one of your function definitions, but leave its declaration and usage in place. You should see an error about a missing or undefined symbol. This illustrates that a declaration is just a hint, with no guarantee that an implementation exists. Notes Link errors can be significantly more difficult to track and resolve than compiler errors. This is partly due to their more cryptic nature, and also because you know your code compiles o.k.! Generally, the undefined symbol error seen here is most common. 7: A Math Header File The preprocessor #include statements we’ve seen so far perform a verbatim include of the referenced file. We can therefore begin to tidy up the main pp6calculator.cpp file by moving the function declarations to a new header file, say PP6Math.hpp, and then #includeing this. Move your function declarations out of your main .cpp file and into a .hpp file, then #include this into the main .cpp file. Compile as before, and it should just work… Notes Not much to say here, other than can you spot, or know, the further steps we’ll need to make to integrate everything nicely and prevent errors? 8: Moving Definitions into the Header Moving the function declarations into a header file has tidied up the main file, but we’ve left the function definitions in there, creating a bit of a disconnect. The easiest thing to do for now is to move the function definitions out of the main file and into your header file after the declarations. After doing this, check that you can still compile pp6calculator o.k. If so, what happens if you now #include your header file twice as shown in the left tile below? Notes Having declarations and definitions in the same header is usually a bad idea, and we’ve done it here to illustrate an error case. There are however some instances where you will want definitions in a header. Performance is one reason, but beware premature optimization! 9: The One Definition (to) Rule (them all) When you #included your header file twice and recompiled, you’ll have probably seen a stream of “error: redefinition of” errors. The reason is that by #includeing the header twice, the compiler sees two definitions of each function in a single translation unit. The C++ Standard states that there can be no more than one definition in any translation unit. In this case the error is rather artificially and deliberately induced, but the next step will show how we can fix this automatically. Notes If you didn’t see an error, then you probably already know how to fix it. If not, can you see what you did in the header file to prevent the ODR causing errors? What would you have to change to see the error? 10: Include Guards You might think the solution is obvious: “just don’t #include the header twice”. True, and the problem is that as soon as the number of files/headers increases, it becomes impossible to keep track of who #includes what from where, so any double inclusion can lead to horrifically nested errors… Instead we let the compiler/preprocessor do the work - using an include guard Enclose the code (declarations and definitions) in your header in the #ifndef/ #define/#endif block as shown in the left hand tile below and try recompiling. Notes All the guard does is prevent the code being included into the same translation unit more than one. Note that the identifier of the #define must be unique. PROJECT_HEADER_HH is usually sufficient, though some add long random hashes as well! 11: A Math Source File In compilation terms we haven’t really changed anything - the inclusion of the header by the main file simply results in the same translation unit as our original monolithic main file. In line with our wish for the header to be a true interface, we want to move the function definitions into their own dedicated .cpp file. Create a new .cpp file to hold your function definitions, giving it the same name as the header, e.g. Math.cpp for Math.hpp. Think about how to compile the overall pp6calculator Notes As shown on the left, the cpp file does not need header guards, but you do need to #include the header contain the function declarations about to be defined. You may also need to add (and maybe remove) the inclusion of some Standard Library headers. 12: Compiling Multiple Sources In fact at the moment this is very simple - we simply list all the sources going into the application in the compile command: $ g++ PP6Math.cpp pp6calculator.cpp \ -o pp6calculator Notes You can also use wildcards, e.g. “*.cpp”. The downside of listing all files is that we recompile everything every time. For example, we might make a minor change to PP6Math.cpp, but we’d still be compiling pp6calculator.cpp, even though nothing in it had changed. 13: Project Organization (Again!) As it stands, our source code is now in reasonable shape, however at the file level we’ve still got a flat organization of files. As you would with other files, it best to use directories to file the sources into logical collections. Create a directory to hold your header and source file, and use git mv to relocate them as shown in the tiles below $ git mv <file> <destination> Notes Again, this is an area where there’s no hard and fast rule. If it’s your own project you can organise as you wish, though logical grouping of files will help to ease the management task. On large project, you may have a set of rules to follow! 14: Complicated Compilation This reorganization of files means we now need to set another compiler flag. If you try compiling the sources again, you’ll get an error about your header file being missing. To tell the g++ compiler where to look, we need to pass it the -I flag and an argument indicating the absolute or relative (to the directory where you run the compiler) path to a directory where headers may be found. You can pass as many -I flags as needed. Notes The compiler has a default search path which you can see by running g++ with the -v flag. Any paths added through -I flags are searched before these default paths. Your #include statements need to list the header using a (relative) path that when added to one of the paths will point to the header. 15: Introducing CMake • We’ve seen some of the problems with compilation that occur when we start to move beyond non trivial programs • We have to list all the files (can use wildcards) • We always recompile everything, rather than just the files that have changed. • In fact, it’s worse than this because whilst we know which files have changed, we not have complete knowledge of which files depend on the changed files, and hence need recompilation. • We have to input all the include paths. • In the next section of the walkthrough we’ll use CMake to resolve these 16: Getting Help With CMake There’s plenty of documentation on the CMake website, and of course man cmake will give a good overview. For quick lookup of the commands, variables and properties that comprise CMake’s scripting language, it’s hard to beat CMake’s command line help interface: $ cmake --help-command project Useful Help Commands --help-variable NAME Print help on variable --help-property NAME Print help on property --help-<type>-list List names of available <type> (command, variable or property) --help-commands List help for all commands 17: A First CMake Script for pp6calculator The first step is to open a text file named CMakeLists.txt in the top level of the project you want to build. In our case, the top level directory is the same as our git repository. CMake requires the file to be named CMakeLists.txt. To begin with we simply add two commands, one to check we’re running a suitable version of CMake, and one to perform minimal system setup for our project. Notes You can supply more detailed version numbers to cmake_minimum_required if you need specific versions. Kitware provide a Version Compatibility Matrix on their wiki You can choose the name of the project as needed. Use cmake --helpcommand for more info! 18: Creating the Build Directory As mentioned earlier, a good practice is to perform the build in a separate directory. Even though our current CMake doesn’t build anything yet, we’ll get this directory set up first to illustrate the usage of CMake. Create a build directory for pp6calculator outside of your repository. The easiest thing to do is create this directory parallel to the repository as shown below: Notes You can create the build directory wherever you want. The parallel creation pattern is simply a convenience and the author’s personal preference. 19: Using CMake to Generate Makefiles With the build directory created, we can now run CMake to generate the build scripts. On UNIX systems, CMake generates Makefiles by default. To see the other build systems supported by CMake, see the “Generators” section of the CMake man page. To generate the Makefiles, simply run the cmake command in your build directory, and pass it the path to your source directory, i.e. the directory holding the CMakeLists.txt file you wrote. Notes You can supply the path as a relative path from your build directory to the source directory, or as a full absolute path. The output you see when running CMake will vary between systems, but you should not see any errors at this point! 20: What CMake has Done Even though we’ve only got two commands in our CMakeLists.txt script, running CMake has performed quite a bit of configuration for us already. If you look at the output from your run of CMake, you should see that it’s searched for, found and checked the functionality of C and C++ compilers for us. It’s also written out the configuration for use, and we’ll look at that in the next step. Notes You can get extremely detailed information on the configuration process by running cmake with the -trace option. You can run cmake as many times as you want try this and note that it does not perform all steps again. It has cached information. 21: What CMake has Generated Once you have run CMake successfully, ls the contents of the build directory, and you’ll find several files and directories created by CMake. The directory CMakeFiles contains all temporary files and checks needed by CMake, and we won’t worry about this for now. The key files to look at are Makefile, the make build script that’s been generated, and CMakeCache.txt, CMake’s cache of settings for this build. Notes We’re ignoring the cmake_install.cmake file for now. We’ll come back to this when we package up the project. 22: CMakeCache.txt This file is used by CMake to persist settings like the compiler and compiler flags so that these do not need to be checked for every time CMake runs. It’s just a text file so you can view it in a pager like less. To explore and edit the cache, you can use the ccmake ncurses interface. In your build directory, type ccmake . to start up the CMake curses frontend. Press ‘t’ to show all of the cache entries and use the up and down arrow keys to navigate the list. Press ‘q’ to quit at any point. Notes You can also use ccmake from the start to configure your project. Simply run ccmake and pass it the path to your source directory, just as you did with cmake earlier. Note that each option is documented in the console bar at the bottom. 23: Makefile This is the script generated by CMake for use with the make build tool. It’s a text file which you can view with a pager like less, and we’ll look at the format of the file in the next step. To use the script, simply type make, and you’ll find it doesn’t do much. CMake generated Makefiles provide a standard interface to Make. To see in detail what Make is doing, try make VERBOSE=1. You can also use make help to see the list of “targets” available. Try building each target by running make <targetname> Main Targets all: Build everything, the target built if you just type make. clean: Remove all built files rebuild_cache: Force rerun of CMake edit_cache: Start up ccmake 24: An Aside on Makefiles The Make build tool is based on the concept of rules, which link a target (a “thing” to be “built”) to the “things” it depends on and the commands needed to “build” the target. Makefiles are just scripts listing these targets and also any needed variables and other functionality. The key thing to note are the dependencies of a target. If the target is newer in time than its dependencies (this assumes target/dependencies are files), then the commands won’t be run. This solves the issue we noted earlier about manual builds always recompiling all files! Notes Targets can depend on other targets, so you can chain them together easily. There can also be targets which are just symbolic names for tasks. We’ve seen this already with the “clean” target. See the GNU Make Manual for further information. 25: Getting CMake to Build pp6calculator Returning to CMake, open your CMakeLists.txt and add the commands shown in the tile below left, adapting them for your organization of sources. include_directories will make CMake configure the compiler to look in the supplied directory for header files. If a relative path is supplied, it is taken as relative to the directory holding the CMakeLists.txt which called include_directories. add_executable configures the build of the program, we supply its name and all its sources. Notes It might seem odd to put a header file into the list of sources for a program. We don’t have to add the .hpp files because CMake will track dependencies automatically. However, if we want to use an IDE like Xcode, then the header won’t appear in the IDE solution unless we list it. 26: Building pp6calculator Change back to your build directory (where you ran CMake before). If you already ran cmake in here, simply type make and you should find that CMake automatically reruns, before building pp6calculator. Try running make again, and you should find nothing changes. Then run make clean and then make again. To see exactly how pp6calculator is built, try make clean followed by make VERBOSE=1. Notes CMake generates build scripts that also track changes to your CMake build scripts. Thus you don’t need to rerun CMake all the time, just run make! If you do need to start from scratch you can simply remove CMakeCache.txt and rerun cmake, or just remove the build directory. 27: Building pp6calculator Fast Make, and CMake generated Makefiles support parallel builds - the ability to use the multicores on modern systems to speed up builds by compiling different pieces of code concurrently. Of course, this is aided by modularization of code. With make, simply use the -jN argument: $ make -j2 Note Generally, setting N to the number of cores is sufficient, though you should be aware of resource limitations on multiuser machines. There are also systems for compiling on clusters, such as distcc. 28: Build Modularization: Libraries We’ve modularized the source code for pp6calculator, but we’re still building a monolithic application. As it turns out, we’ll continue to do this, but it’s useful to see how we can use CMake to partition the build into separate components of binary code - libraries. Use the add_library command to create a library from the headers/sources other than your main program. Change add_executable to only take the source for the main program. Finally, use target_link_libraries to link the library to the program. Notes Note that in target_link_librarie s we use the symbolic names for the program and library. These are internal to CMake and, as we’ll see, don’t correspond to the names of the resultant files on disk. 29: Static Libraries With your CMakeLists.txt file edited, go back to your build directory (a good tip is to keep two terminal windows open!) and run make again. CMake should reconfigure and rebuild, leaving you with pp6calculator plus a new file, libpp6lib.a, a static library. This contains the pre-compiled code for the math functions, and by linking it into pp6calculator, we have copied the needed blocks of binary into the executable. This makes the binary larger, but as we are only using the library in a single executable, this is o.k. Notes To use any library, you simply need to #include the relevant header file(s) with the declarations of the library interface in your source code. You then simply link the library to your final executable. It’s this feature that leads to reusable code! 30: Shared Libraries By default, CMake builds static libraries, but we can also get it to build and use shared, or dynamic libraries. To do this, add the SHARED argument to your add_library command as shown below. In your build directory, run make clean followed by make. You should have pp6calculator as before, and a different library, libpp6lib.so. Note that the tile below shows a different name, libpp6lib.dylib. Different platforms have different extensions for shared libraries! Notes You can also change the type of library built by default using the cmake BUILD_SHARED_LIBS variable passed as an argument, e.g. $ cmake \ -DBUILD_SHARED_LIBS=ON \ ../pp6calculator.git 31: Using Shared Libraries Once you’ve got your shared library built, run ldd pp6calculator on Linux, or otool -L pp6calculator on OS X. These commands show the shared libraries used by a program. Shared libraries still contain binary code and are used in C++ code in the same way as static libraries. The difference arises in the linking step, with shared libraries being loaded into the executable at runtime. The linker uses an addressing scheme so the code used by the application can be located at startup. Many applications can use the same shared library. Notes Shared libraries are the preferred mode for system libraries because they can be upgraded dynamically. Applications using static libraries must be recompiled when the library changes. In our case though, pp6lib is private to the application, so static libraries are actually better because we do not have to worry about shared library lookup issues. 32: Back to Static Modules Because pp6lib is private to our application, i.e. it’s not intended for third party use, we’ll force it to be a static library, and also modularize the build further using add_subdirectory. Create a new CMakeLists.txt file in the subdirectory holding your library code, and move the add_library command into this file, adjusting the file paths and using the STATIC flag. Use the add_subdirectory command in the top level CMakeLists.txt to make CMake recurse into the library subdirectory. Rebuilding should work! Notes You can add arbitrary levels of nesting to CMake. However, beware that too many levels may result in slower build performance. You can of course always have a single top level CMakeLists.txt script and directly refer to everything here. 33: Compiler Flags When you ran make VERBOSE=1 earlier, you may have noticed that CMake did not use some of the useful compiler flags we’ve seen earlier like -W -Wall. You can change the flags used by setting the CMake variable CMAKE_CXX_FLAGS. As flags are compiler specific, we need to guard the setting of this variable by a check on which compiler is used. We’re using the GNU compiler, so set CMAKE_CXX_FLAGS to the set shown below, using the CMAKE_COMPILER_IS_GNUCXX variable to guard the setting. Notes Of course, check that the flags are passed to the compiler by using make VERBOSE=1 The list of flags we’ve chosen provides very strict checking for errors and compliance with ISO C+ +98. You can get more details on each flag in the man page for g++. 33a: Compiler Flags: C++11 In general, compilers do not default to using C++11, even if they support it, so an additional flag is needed to select the standard. We saw this for GCC/Clang in the previous slide with the std=c++98 flag. To enable C++11, one could just change this to -std=c++11, but the compiler would need to support this. The example below shows how we can choose to enable C++11 support if it is available in the GCC or Clang compiler being used. Notes Of course, check that the flags are passed to the compiler by using make VERBOSE=1 Note that this is not the whole story of enabling support for C++11. You may need to consider which features are supported, and the Standard Library in use (e.g. libstdc++ vs libc++). 34: Finishing Up: Packaging and Installing As it stands, our CMake scripts build everything for us but we can’t install pp6calculator for later use or by others. To enable this, use the install command and include CMake’s CPack module as shown below in your CMakeLists.txt and rebuild. You should now have two new make targets, package and package_source. Build them and see what’s generated. Notes CMake’s CPack module has lots of settings to help configure what is packaged and how the packages are named. To see the CPack variables you can set in your CMakeLists.txt, run cpack --help-variables There’s also a helpful, if basic, tutorial on CPack on the CMake wiki. 35: A Note on Installing The addition of the install command means that you can install pp6calculator directly to the system using make install. However, if you try this, you’ll probably get a permissions error because CMake’s default directory for installation is /usr/local. Try changing CMAKE_INSTALL_PREFIX to say, ~/Software, by running make edit_cache and editing its entry. Reconfigure, rebuild and try running make install. Notes Rather than using edit_cache, you can also just run cmake again and pass CMAKE_INSTALL_PREFIX as a command line argument, e.g. $ cmake \ -DCMAKE_INSTALL_PREFIX=~/ Software \ ../pp6calculator.git Walkthrough Summary We’ve worked through the steps to modularize both the source code and build of pp6calculator. We’ve seen how to separate functions into declarations and definitions using header and source files. Header files and function declarations have provided our first example of an interface. We’ve used CMake to automate the build of pp6calculator from its modularized sources, and have introduced code libraries to modularize the build. We also packaged up pp6calculator. Further Reading CMake Documentation Software Build Systems Textbook Comparing your work with upstream • If you cloned my repository to work with, you’ll have made and committed changes yourself to your local clone on the “mysolutions” branch • If you’re curious to compare your solutions with mine, then you can do the following to pull changes and compare: $ $ $ * git checkout master git pull origin git branch master mysolutions $ git diff master mysolutions Day 2 Homework: Aims • Simplify menu interface of pp6calculator to partition functionality and allow for future extensions • Provide an operation to generate 100 random energies and momenta and report the mean energy, its standard deviation and the energy and momentum of the highest energy particle. • Provide an operation to analyse a data file provided by the user listing particle type, energy and momentum • Pick out mu+ and mu- particles observed in “run4.dat” • Calculate the invariant mass of each mu+/mu- combination and report the largest 10 invariant masses and which combinations formed these Day 2 Homework Part 1 • Provide a two level menu interface for pp6calculator • The top level menu should allow the user to go into one to N submenus of operations, partitioned by the day of the course they were implemented on. • Each submenu should provide an interface to the operations for a given day of the course, essentially the same interface you implemented on Day 1. • Hint: Use functions and headers to modularize the code between the top level application, menu interface and operation implementation. • Hint: Having said that, use your judgment as to how much or how little to modularize, and experiment with this. Day 2 Homework Part 2 • Provide an operation to generate 100 random energies and 3-momenta and report the mean energy and its standard deviation • Hint: To generate (pseudo)random numbers in C++, use the following: #include <cstdlib> // At the top ... int a = rand()%100 // Random number between 0-99 • Hint: Start by generating the momentum components, and then generate a random mass to calculate the energy. • Hint: Think about how to adapt your sort function so that it will return an array whose entries are the indices of the sorted entries in the main array. For example, if the input array was {2,5,3,7}, the index array would contain {3,1,2,0}. Day 2 Homework Part 3 • Provide an operation to analyse a data file provided by the user listing particle type, energy and momentum • Pick out mu+ and mu- particles observed in “run4.dat” • Calculate the invariant mass of each mu+/mu- combination and report the largest 10 invariant masses and which mu+/mu- events formed these • The data file to be analysed can be downloaded from the webpage for Day 2. • The filename is “ObservedParticles.dat”, it’s a simple text file so the format can be read directly Day 2 Homework Part 3 • To help with reading the file, you should use the FileReader library supplied on the course webpage. • You can either copy the sources files into your library, or unpack the FileReader source package into your repository and use CMake’s add_subdirectory and target_link_libraries just as you do for your library. • See the README.md file in the FileReader package for documentation on how to use it to read files and extract values from each line. • Hint: Note that by forming pairs, we mean calculate the invariant masses formed by pairing each mu- with every other mu+ in sequence (i.e. it’s combinatoric). Use the index based sort you looked at in Part 2 to present the 10 highest invariant masses and their associated mu+/mu- event pairs