Struct Looper: an utility to apply a callable on arbitrary structure variables in a loop
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:
- A list of lambda-calls must be repeated for each variable in different places.
- 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.