Product aggregation details

Aggregation behavior

The automatic aggregation that art performs is explained in the table below. For non-user-defined types, art provides a default aggregation behavior.

Product type Aggregation behavior
User-defined Call user-defined aggregate function (see below)
Arithmetic2 Call operator+=
std::string Throw exception if values are not equal
std::vector<T> Append elements
std::pair<T1,T2> Element-wise aggregation
std::map<K,V> Insert elements
CLHEP::HepVector Call operator+=
TH1-derived classes Call Add

2 As determined by std::is_arithmetic.

3 Not currently supported in ROOT6.

User-defined aggregation

For any product that is 'put' using the art::runFragment or art::subRunFragment function calls, a compile-time check is performed that validates the presence of an appropriate aggregation behavior. For user-defined types, a public aggregate function must be provided with following signature:

class MyProduct {


  void aggregate(MyProduct const&) 


where the user determines the semantic for aggregating product instances.

Customizing aggregation for types with art-provided default behavior

Suppose a Run product has been created that associates the number of identified particles for that run with a given particle ID--i.e. the type is std::map<PDGID,unsigned>, where the unsigned mapped-type represents a counter. In the producer module, the following code would be seen:

void MyProducer::endRun(Run& r) override
   auto pidMap = std::make_unique<std::map<PDGID,unsigned>>(...);
   r.put(std::move(pidMap), art::runFragment());

If the product in one file has the value:

# File 1
   [-11, 22] # 22 positrons
   [ 11, 24] # 24 electrons
   [ 13,  2] # 2 muons

and the product in another files has the value:

# File 2
   [ 13,  3] # 3 muons
   [-13,  2] # 7 anti-muons

upon product aggregation, the final result would be:

# Aggregated ParticleIDMap instance
   [-11, 22] # 22 positrons
   [ 11, 24] # 24 electrons
   [ 13,  2] # 2 muons - !!! NOT 5 !!!
   [-13,  2] # 7 anti-muons

In this case, one would reasonably assume that the muon entry (key of '13') should have a mapped-value of 5. However, as the default aggregation behavior for std::map is to call insert, the attempt to insert the muon entry from the second map fails because one already existed in the first map. To solve this problem, the std::map object should be encapsulated in its own user-defined class with a dedicated aggregate function:

class ParticleIDMap {

  ParticleIDMap() = default; // Needed for ROOT

  void aggregate(ParticleIDMap const& other)
    // Implementation to appropriately combine maps

  std::map<PDGID,unsigned> particles_;

This technique can be applied to any of the types above that have a default aggregation behavior.