2.2. The code

The program we're going to present and annotate here:

A secondary lesson from this program is the use of the std::unique_ptr template class of the C++ standard library to do storage management. We'll say more about that as we look at the code.

Let's look at the includes and declarations:


#include <CDataSource.h>               (1)
#include <CDataSourceFactory.h>        (2)
#include <CRingItem.h>                 (3)
#include <DataFormat.h>                (4)
#include <Exception.h>                 (5)

                                        

#include <iostream>                          (6)
#include <cstdlib>
#include <memory>
#include <vector>
#include <cstdint>

static void
processRingItem(CRingItem& item);       (7)

                

(1)
The CDataSource.h header defines the data source base class. We don't need to include the headers for the concrete classes because we're only going to be dealing with pointers so the base class object shape is all the compiler needs to see.
(2)
CDataSourceFactory.h defines the data source factory class that we're going to use to create our data source given the data source URI the user passes on the command line.
(3)
Since the data source is going to give us CRingItem objects and since we're going to invoke methods on that object we also need to include the class definition for CRingITem
(4)
The DataFormat.h header contains detailed structure definitions for the ring items. We won't need that information because the CRingItem class insulates us from the details of ring item formats. What we do need from this header are symbolic definitions for the ring item types.
(5)
CException defined in the Exception.h header is a base class for the exceptions NSCLDAQ throws. Several of the object methods we invoke can throw exceptions that are derived from this base class. We'll just catch CException& so that we only need to know the base class interface.
(6)
These headers are part of the C++ standard run time library. In some cases (e.g. cstdlib), we could use C standard run time library headers instead had we chosen. The reason we don't is that we want to illustrate how C standard library functions are encapsulated in the std namespace that the C++ standard run-time library uses.

We'll use a helper function usage to output program usage and exit in error. Here's the definition and implementation of that function.


static void
usage(std::ostream& o, const char* msg)
{
    o << msg << std::endl;
    o << "Usage:\n";
    o << "  readrings uri\n";
    o << "      uri - the file: or tcp: URI that describes where data comes from\n";
    std::exit(EXIT_FAILURE);
}

                

Nothing really remarkable about this. Note, however how the exit(3) function is encapsulated in the std namespace though the return codes it supports are not. This is because those return codes are preprocessor descriptions and those can't easily be embedded in a namespace.

The first part of the main program ensures we have a URI parameter so that we won't segfault trying to reference an argument that does not exist. We then use that argument to create a data source:


int
main(int argc, char** argv)
{
    if (argc != 2) {                           (1)
        usage(std::cerr, "Not enough command line parameters");
    }
    std::vector<std::uint16_t> sample;      (2)
    std::vector<std::uint16_t> exclude;     (3)
    CDataSource* pDataSource;
    try {
        pDataSource =                             (4)
            CDataSourceFactory::makeSource(argv[1], sample, exclude);
    }
    catch (CException& e) {                  (5)
        usage(std::cerr, "Failed to open ring source");
    }

            

(1)
This code ensures a parameter was provided on the command line for the data source URI. If not the usage function is called to output an error message and to exit.
(2)
Data sources that are online can be told to sample data of various types. By sampling we mean that if the consumer gets behind the producer, the data source is allowed to skip data of that type. The sample vector will hold the set of ring item types that can be sampled. Our example doesn't want to skip any ring item types so this vector will be empty.
(3)
In addition to specifying that some ring item types are elligible for sampling, a data source can filter out some ring item types altogether. The exclude vector will be a vector of the ring item types that can be filtered out. Once more, we don't want to miss any of the ring item types, so we leave the vector empty.
(4)
This call uses the data source factory to create and return a pointer to a data source of the appropriate type. The makeSource static method analyzes the URI passed as its first parameter and uses that to create the appropriate data source. Note that there's a special case. If, instead of a URI, the data source name is -, data will be taken from standard input allowing the program to be a pipeline element.

The additional parameters specify the ring item types that can be sampled or filtered out entirely.

The creation of the data source is done inside a try block because it reports errors by throwing exceptions that are objects subclassed from CException.

(5)
This catch block is executed if CDataSourceFactory::makeSource throws an exception. In this example, it only uses usage to output a generic message and exit. If you want a more informative error message, you can construct one using the ReasonText method of CException as that method returns a string that describes the reason the exception was thrown.

Now that we've created a data source, let's look at the main loop of the program. The loop accepts data from the data source and passes it off to processRingItem for processing.


    CRingItem*  pItem;
    while ((pItem = pDataSource->getItem() )) {       (1)
        std::unique_ptr<CRingItem> item(pItem);    (2)
        processRingItem(*item);                          (3)
    }

    std::exit(EXIT_SUCCESS);
}
            

(1)
The main loop accepts CRingItem objects from the data source. If the data source has no more items (can only happen for file data sources), getItem returns a null pointer and the while loop exits.
(2)
The pointer returned from getItem has to be deleted once you are done with it as it was dynamically allocated. The templated class std::unique_ptr is a pointer like object that manages a dynamically allocated pointer. Once destroyed (as it is automatically when it goes out of scope at the end of the block controlled by the while loop), it also destroys the pointer it manages.

The std::unique_ptr is called pointer like because it implements the unary operator* and operator->. These operators are implemented in such a way that a std::unique_ptr is virtually indistinguishable from the pointer it manages.

The name std::unique_ptr means that only one of these should manage any given pointer. To that end, the object does not support assignment or copy construction. If you need more relaxed semantics you can look at std::shared_ptr which uses reference counting to determine when to delete the managed pointer.

(3)
Dereferencing the unique pointer produces a reference to the ring item, which is what processRingItem expects. So, for each ring item received from the data source, processRingItem is used to do whatever processing is desired.

Finally the processsRingItem function. This is really just a placeholder for whatever processing you'd like to perform. All we do in the example is determine if the ring item was a physics event (response to an experiment trigger) and, if so output that fact.


static void
processRingItem(CRingItem& item)
{
    if (item.type() == PHYSICS_EVENT) {
        std::cout << "Got a physics item\n";
    }
}
            

The constant PHYSICS_EVENT is defined in the header DataFormat.h. It is one of several supported ring item types. The type method of CRingITem returns the ring items's type.