Skip to content

Struct Looper: an utility to apply a callable on arbitrary structure variables in a loop

Sergei Zharko requested to merge s.zharko/cbmroot:struct-looper into master

Idea

There are many use-cases to handle similar variables in a plane structure (class). E.g., setup units:

struct Setup {
  std::optional<mvd::Unit> fMvd;
  std::optional<sts::Unit> fSts;
  std::optional<trd::Unit> fTrd;
  std::optional<tof::Unit> fTof;
  std::optional<rich::Unit> fRich;
  std::optional<much::Unit> fMuch;
  std::optional<fsd::Unit> fFsd;
  std::optional<bmon::Unit> fBmon;
};

All the Unit classes have a similar API, but do not share a common virtual interface.

In many cases, there is a need to apply the same task to all of them (e.g., serialization), or a part of them (only, if they have a value). To do this, one has to implement functions like this:

void Print(const Setup& s) const 
{
  auto PrintUnit = [](const auto& unit) {
    if (unit.has_value()) {
      LOG(info) << unit.ToString();
    }
  }
  PrintUnit(s.fMvd);
  PrintUnit(s.fSts);
  PrintUnit(s.fTrd);
  PrintUnit(s.fTof);
  PrintUnit(s.fRich);
  PrintUnit(s.fMuch);
  PrintUnit(s.fFsd);
  PrintUnit(s.fBmon);
}

and

template<class Archive>
void Setup::save(Archive& ar, const unsigned int)
{
  auto SaveUnit = [&ar](auto&& unit) {
    if (unit.has_value()) {
      ar << true;
      ar << unit.value();
    }
    else {
      ar << false;
    }
  };

  SaveUnit(fMvd);
  SaveUnit(fSts);
  SaveUnit(fTrd);
  SaveUnit(fTof);
  SaveUnit(fRich);
  SaveUnit(fMuch);
  SaveUnit(fFsd);
  SaveUnit(fBmon);
}

The examples above are quite trivial, more complex action on each variable may be needed.

Such an approach has two main problems:

  1. A list of lambda-calls must be repeated for each variable in different places.
  2. Lambda-functions may be stored as named objects, which have a type of std::funciton. It brings performance penalties.

Struct Looper

The struct looper is a simple template class, which solves these problems, providing a compile-time tuple of pointer to the selected structure variables, and a for-each method, which is applied in a loop to each variable.

With it, the code above can be rewritten as follows:

struct Setup {
  std::optional<mvd::Unit> fMvd;
  std::optional<sts::Unit> fSts;
  std::optional<trd::Unit> fTrd;
  std::optional<tof::Unit> fTof;
  std::optional<rich::Unit> fRich;
  std::optional<much::Unit> fMuch;
  std::optional<fsd::Unit> fFsd;
  std::optional<bmon::Unit> fBmon;
 
  /// \brief A for-each method, which is applied to each VARIABLE.
  /// \param visitor  A callable visitor
  /// \note  If the variable is std::optional<T>, the argument of the visitor is treated as std::optional<T>.
  template<class Visitor>
  void ForEachValue(Visitor&& visitor)
  {
    kLooper.ForEachValue(this, std::forward<Visitor>(visitor));
  }

  /// \brief A for-each method, which is applied to each VARIABLE.
  /// \param visitor  A callable visitor
  /// \note  If the variable is std::optional<T>, the argument of the visitor is treated as std::optional<T>.
  template<class Visitor>
  void ForEachValue(Visitor&& visitor) const
  {
    kLooper.ForEachValue(this, std::forward<Visitor>(visitor));
  }

  /// \brief A for-each method, which is applied to a VALUE stored in each variable
  /// \param visitor  A callable visitor
  /// \note  If the variable is std::optional<T>, the argument of the visitor is T
  template<class Visitor>
  void ForEachVariable(Visitor&& visitor)
  {
    kLooper.ForEachVariable(this, std::forward<Visitor>(visitor));
  }

  /// \brief A for-each method, which is applied to a VALUE stored in each variable
  /// \param visitor  A callable visitor
  /// \note  If the variable is std::optional<T>, the argument of the visitor is T
  template<class Visitor>
  void ForEachVariable(Visitor&& visitor) const
  {
    kLooper.ForEachVariable(this, std::forward<Visitor>(visitor));
  }

 private:
  static constexpr auto kLooper = cbm::util::StructLooper(
    &Setup::fMvd, &Setup::fSts, &Setup::fTrd, &Setup::fTof, &Setup::fRich, &Setup::fMuch, &Setup::fFsd, &Setup::fBmon);
};

where the methods ForEachVariable executes a callable visitor on each variable, and ForEachValue executes a visitor on a value, stored in each variable. The implementation of these methods are the same for each class, so they can be automatically generated with a preprocessor macro.

The functions Print and save would then look as follows:

void Print(const Setup& s) const 
{
  s.ForEachValue([](const auto& unit) { LOG(info) << unit.ToString();});
}
template<class Archive>
void Setup::save(Archive& ar, const unsigned int)
{
  this->ForEachVariable([&ar](auto&& unit) {
    if (unit.has_value()) {
      ar << true;
      ar << unit.value();
    }
    else {
      ar << false;
    }
  });
}

I tried to find something similar in STL or BOOST, but I did not.

Edited by Sergei Zharko

Merge request reports

Loading