Skip to content

Commit

Permalink
Rework the I/O handling for relations (#673)
Browse files Browse the repository at this point in the history
* Rework the I/O handling for relations

This is work that has become necessary for making links work that
contain interface types.

- Remove differences in handling of interface vs regular types in the
jinja2 templates and lift it into a separate function that dispatches
depending on the type of the relation
- Make the interfaced_types type member publicly available for interface
types
- Add type helpers to detect whether a type is an interface type

* Install implementation headers are installed

* Make things work with c++17

* Use more suitable name for header

* Remove parts that are only required later

* Make things work again (again) with c++17

* Remove check that is not part of this feature

* Make lambda capture simpler
  • Loading branch information
tmadlener authored Sep 24, 2024
1 parent 0fa08d8 commit 86e2ba9
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 31 deletions.
176 changes: 176 additions & 0 deletions include/podio/detail/RelationIOHelpers.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#ifndef PODIO_DETAIL_RELATIONIOHELPERS_H
#define PODIO_DETAIL_RELATIONIOHELPERS_H

#include "podio/utilities/TypeHelpers.h"
#include <podio/CollectionBase.h>

#include <tuple>
#include <vector>

namespace podio::detail {

/// Function template for handling interface types in OneToMultiRelations
///
/// Effectively this function checks whether the passed collection can be
/// dynamically cast to the collection type of the concrete type and if that is
/// true uses it to construct an interface type and add it to relElements. The
/// function on its own doesn't do anything too meaningful, it is meant to be
/// used in a call to std::apply that goes over all the interfaced types of an
/// interface type.
///
/// @tparam T The concrete type inside the interface that should be checked.
/// This effectively is mainly used for tag-dispatch and overload
/// selection in this context
/// @tparam InterfaceType The interface type (that can be used to interface T)
///
/// @param relElements The vector to which the interface objects should be added
/// @param coll The collection that holds the actual element
/// @param id The ObjectID of the element that we are currently looking for
template <typename T, typename InterfaceType>
void tryAddTo(T, std::vector<InterfaceType>& relElements, const podio::CollectionBase* coll, const podio::ObjectID id) {
if (auto typedColl = dynamic_cast<const typename T::collection_type*>(coll)) {
const T tmp = (*typedColl)[id.index];
relElements.emplace_back(tmp);
}
}

/// Helper function for handling interface type relations in OneToManyRelations
///
/// This function tries all types that are interfaced by the InterfaceType and
/// adds the one that matches to the relations. The main work happens in
/// tryAddTo, this simply wraps everything in a std::apply over all
/// interfaced_types.
///
/// @tparam InterfaceType The interface type of the Relation
///
/// @param relElements The vector to which the interface objects should be added
/// @param coll The collection that holds the actual element
/// @param id The ObjectID of the element that we are currently looking for
template <typename InterfaceType>
void addInterfaceToMultiRelation(std::vector<InterfaceType>& relElements, const podio::CollectionBase* coll,
const podio::ObjectID id) {
std::apply([&](auto... t) { (tryAddTo(t, relElements, coll, id), ...); }, typename InterfaceType::interfaced_types{});
}

/// Helper function for adding an object to the OneToManyRelations container
/// when reading back collections
///
/// This function does the necessary type casting of the passed collection to
/// retrieve the desired object and also takes care of adding the object to the
/// container that holds them for later usage. It handles relations to regular
/// types as well as interface types.
///
/// This functionality has been lifted from the jinja2 templates, where we now
/// only call it, because we need a template deduction context for making if
/// constexpr work as expected, such that we can dispatch to different
/// implementatoins depending on whether the relation is to an interface type or
/// to a regular type.
///
/// @note It is expected that the following pre-conditions are met:
/// - The passed collection is valid (i.e. not a nullptr)
/// - the collectionID of the passed collection is the same as the one in
/// the passed ObjectID
/// - The collection can by casted to the relation type or any of the
/// interfaced types of the relation
///
/// @tparam RelType The type of the OneToManyRelation
///
/// @param relElements The container that holds the objects for the relation and
/// which will be used for adding an element from the passed
/// collection
/// @param coll The collection from which the object will be obtained after the
/// necessary type casting
/// @param id The ObjectID of the object that should be retrieved and added.
template <typename RelType>
void addMultiRelation(std::vector<RelType>& relElements, const podio::CollectionBase* coll, const podio::ObjectID id) {
if constexpr (podio::detail::isInterfaceType<RelType>) {
addInterfaceToMultiRelation(relElements, coll, id);
} else {
const auto* typeColl = static_cast<const typename RelType::collection_type*>(coll);
relElements.emplace_back((*typeColl)[id.index]);
}
}

/// Function template for handling interface types in OneToOneRelations
///
/// Effectively this function checks whether the passed collection can be
/// dynamically cast to the collection type of the concrete type and if that is
/// true uses it to assign to the passed interface object The function on its
/// own doesn't do anything too meaningful, it is meant to be used in a call to
/// std::apply that goes over all the interfaced types of an interface type.
///
/// @tparam T The concrete type inside the interface that should be checked.
/// This effectively is mainly used for tag-dispatch and overload
/// selection in this context
/// @tparam InterfaceType The interface type (that can be used to interface T)
///
/// @param relation The object to which the interface object should be assigned
/// to
/// @param coll The collection that holds the actual element
/// @param id The ObjectID of the element that we are currently looking for
template <typename T, typename InterfaceType>
void tryAssignTo(T, InterfaceType*& relation, const podio::CollectionBase* coll, const podio::ObjectID id) {
if (const auto* typeColl = dynamic_cast<const typename T::collection_type*>(coll)) {
relation = new InterfaceType((*typeColl)[id.index]);
}
}

/// Helper function for handling interface type relations in OneToOneRelations
///
/// This function tries all types that are interfaced by the InterfaceType and
/// that assigns the one thta matches to the relation. The main work happens in
/// tryAssignTo, this simply wraps everything in a std::apply over all
/// interfaced_types.
///
/// @tparam InterfaceType The interface type of the Relation
///
/// @param relation The object to which the interface object should be assigned
/// to
/// @param coll The collection that holds the actual element
/// @param id The ObjectID of the element that we are currently looking for
template <typename InterfaceType>
void addInterfaceToSingleRelation(InterfaceType*& relation, const podio::CollectionBase* coll,
const podio::ObjectID id) {
std::apply([&](auto... t) { (tryAssignTo(t, relation, coll, id), ...); }, typename InterfaceType::interfaced_types{});
}

/// Helper function for assigning the related object in a OneToOneRelation
///
/// This function does the necessary type casting of the passed collection to
/// retrieve the desired object and also takes care of assigning the object to
/// the passed pointer that hold it for later usage. It handles relations
/// to regular types as well as interface types.
///
/// This functionality has been lifted from the jinja2 templates, where we now
/// only call it, because we need a template deduction context for making if
/// constexpr work as expected, such that we can dispatch to different
/// implementatoins depending on whether the relation is to an interface type or
/// to a regular type.
///
/// @note It is expected that the following pre-conditions are met:
/// - The passed collection is valid (i.e. not a nullptr)
/// - the collectionID of the passed collection is the same as the one in
/// the passed ObjectID
/// - The collection can by casted to the relation type or any of the
/// interfaced types of the relation
///
/// @tparam RelType The type of the OneToManyRelation
///
/// @param relation The pointer to which we should assign the related object
/// that is retrieved from the passed collection
/// @param coll The collection from which the object will be obtained after the
/// necessary type casting
/// @param id The ObjectID of the object that should be retrieved and added.
template <typename RelType>
void addSingleRelation(RelType*& relation, const podio::CollectionBase* coll, const podio::ObjectID id) {
if constexpr (podio::detail::isInterfaceType<RelType>) {
addInterfaceToSingleRelation(relation, coll, id);
} else {
const auto* typeColl = static_cast<const typename RelType::collection_type*>(coll);
relation = new RelType((*typeColl)[id.index]);
}
}

} // namespace podio::detail

#endif // PODIO_DETAIL_RELATIONIOHELPERS_H
10 changes: 10 additions & 0 deletions include/podio/utilities/TypeHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,16 @@ namespace detail {
template <typename Tuple>
using TupleOfMutableTypes = typename ToTupleOfTemplateHelper<GetMutableHandleType, Tuple>::type;

/// Detector for checking for the existence of an interfaced_type type member
template <typename T>
using hasInterface_t = typename T::interfaced_types;

/// Variable template for checking whether the passed type T is an interface
/// type.
///
/// @note: This simply checks whether T has an interfaced_types type member.
template <typename T>
constexpr static bool isInterfaceType = det::is_detected_v<hasInterface_t, T>;
} // namespace detail

// forward declaration to be able to use it below
Expand Down
2 changes: 2 additions & 0 deletions python/templates/CollectionData.cc.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
{{ include }}
{% endfor %}

#include <podio/detail/RelationIOHelpers.h>

{{ utils.namespace_open(class.namespace) }}
{% with class_type = class.bare_type + 'CollectionData' %}

Expand Down
11 changes: 7 additions & 4 deletions python/templates/Interface.h.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@

{{ common_macros.class_description(class.bare_type, Description, Author) }}
class {{ class.bare_type }} {
public:
/// type alias containing all the types this interface should work for in a
/// tuple
using interfaced_types = std::tuple<{{ Types | join(", ")}}>;

/// type alias containing all the types this interface should work for.
using InterfacedTypes = std::tuple<{{ Types | join(", ")}}>;
private:
/// type alias containing all the mutable types that can be used to initialize
/// this interface
using InterfacedMutableTypes = podio::detail::TupleOfMutableTypes<InterfacedTypes>;
using InterfacedMutableTypes = podio::detail::TupleOfMutableTypes<interfaced_types>;

/// template variable for determining whether type T is a valid interface type
template<typename T>
constexpr static bool isInterfacedType = podio::detail::isInTuple<T, InterfacedTypes>;
constexpr static bool isInterfacedType = podio::detail::isInTuple<T, interfaced_types>;

/// template variable for determining whether type T can be used to initialize
/// this interface
Expand Down
27 changes: 2 additions & 25 deletions python/templates/macros/collections.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,7 @@ std::vector<{{ member.full_type }}> {{ class.bare_type }}Collection::{{ member.n
m_rel_{{ relation.name }}->emplace_back({{ relation.full_type }}::makeEmpty());
continue;
}
{% if relation.interface_types %}
// We need the concrete collection type to assign it to an InferenceWrapper
{% set else = joiner("else") %}
{% for int_type in relation.interface_types %}
{{ else() }} if (auto {{ int_type.bare_type }}Coll = dynamic_cast<{{ int_type.full_type }}Collection*>(coll)) {
const auto tmp = (*{{ int_type.bare_type }}Coll)[id.index];
m_rel_{{ relation.name }}->emplace_back(tmp);
}
{% endfor %}
{% else %}
{{ relation.full_type }}Collection* tmp_coll = static_cast<{{ relation.full_type }}Collection*>(coll);
const auto tmp = (*tmp_coll)[id.index];
m_rel_{{ relation.name }}->emplace_back(tmp);
{% endif %}
podio::detail::addMultiRelation(*m_rel_{{ relation.name }}, coll, id);
} else {
m_rel_{{ relation.name }}->emplace_back({{ relation.full_type }}::makeEmpty());
}
Expand All @@ -102,17 +89,7 @@ std::vector<{{ member.full_type }}> {{ class.bare_type }}Collection::{{ member.n
entries[i]->m_{{ relation.name }} = nullptr;
continue;
}
{% if relation.interface_types %}
{% set else = joiner("else") %}
{% for int_type in relation.interface_types %}
{{ else() }} if (auto {{ int_type.bare_type }}Coll = dynamic_cast<{{ int_type.full_type }}Collection*>(coll)) {
entries[i]->m_{{ relation.name }} = new {{ relation.full_type }}((*{{ int_type.bare_type }}Coll)[id.index]);
}
{% endfor %}
{% else %}
{{ relation.full_type }}Collection* tmp_coll = static_cast<{{ relation.full_type }}Collection*>(coll);
entries[i]->m_{{ relation.name }} = new {{ relation.full_type }}((*tmp_coll)[id.index]);
{% endif %}
podio::detail::addSingleRelation(entries[i]->m_{{ relation.name }}, coll, id);
} else {
entries[i]->m_{{ relation.name }} = nullptr;
}
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ install(FILES ${headers_necessary}
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/podio
)
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/podio/utilities
${PROJECT_SOURCE_DIR}/include/podio/detail
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/podio
)

Expand Down
9 changes: 7 additions & 2 deletions tests/unittests/interface_types.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#include "catch2/catch_test_macros.hpp"

#include "podio/ObjectID.h"

#include "datamodel/ExampleHitCollection.h"
#include "datamodel/MutableExampleCluster.h"
#include "datamodel/TypeWithEnergy.h"

#include "podio/ObjectID.h"
#include "podio/utilities/TypeHelpers.h"

#include <map>
#include <stdexcept>

Expand Down Expand Up @@ -45,6 +46,10 @@ TEST_CASE("InterfaceTypes basic functionality", "[interface-types][basics]") {
REQUIRE(wrapper1.id() == podio::ObjectID{0, 42});
}

TEST_CASE("InterfaceTypes static checks", "[interface-types][static-checks]") {
STATIC_REQUIRE(podio::detail::isInterfaceType<TypeWithEnergy>);
}

TEST_CASE("InterfaceTypes STL usage", "[interface-types][basics]") {
// Make sure that interface types can be used with STL map and set
std::map<TypeWithEnergy, int> counterMap{};
Expand Down

0 comments on commit 86e2ba9

Please sign in to comment.