Skip to content

Interopping with containers in third-party libraries #213

@SidneyCogdill

Description

@SidneyCogdill

I attempt to use flux with immer::vector (I know, that's a weird thing to do. immer iterators are already const and never invalidates). For the most part (except for .to<immer::vector<T>>()) it seems to work as long as this custom trait is defined:

The trait implementation
template<typename T>
struct flux::sequence_traits<immer::vector<T>>
{
    using value_type = std::ranges::range_value_t<immer::vector<T>>;

    static constexpr auto first(auto &) -> index_t { return index_t{0}; }

    static constexpr auto is_last(auto &self, index_t idx) { return idx >= size(self); }

    static constexpr auto inc(auto &self, index_t &idx)
    {
        FLUX_DEBUG_ASSERT(idx < size(self));
        idx = num::checked_add(idx, distance_t{1});
    }

    static constexpr auto read_at(auto &self, index_t idx) -> decltype(auto)
    {
        indexed_bounds_check(idx, size(self));
        return self[idx];
    }

    static constexpr auto read_at_unchecked(auto &self, index_t idx) -> decltype(auto)
    {
        return self[idx];
    }

    static constexpr auto dec(auto &, index_t &idx)
    {
        FLUX_DEBUG_ASSERT(idx > 0);
        idx = num::checked_sub(idx, distance_t{1});
    }

    static constexpr auto last(auto &self) -> index_t { return size(self); }

    static constexpr auto inc(auto &self, index_t &idx, distance_t offset)
    {
        FLUX_DEBUG_ASSERT(num::checked_add(idx, offset) <= size(self));
        FLUX_DEBUG_ASSERT(num::checked_add(idx, offset) >= 0);
        idx = num::checked_add(idx, offset);
    }

    static constexpr auto distance(auto &, index_t from, index_t to) -> distance_t
    {
        return num::checked_sub(to, from);
    }

    static constexpr auto size(auto &self) -> distance_t
    {
        return checked_cast<distance_t>(std::ranges::ssize(self));
    }

    static constexpr auto for_each_while(auto &self, auto &&pred) -> index_t
    {
        auto iter = std::ranges::begin(self);
        auto const end = std::ranges::end(self);

        while (iter != end) {
            if (!std::invoke(pred, *iter)) {
                break;
            }
            ++iter;
        }

        return checked_cast<index_t>(iter - std::ranges::begin(self));
    }
};

You just duplicate the default implementation for contiguous containers and make minimal adaptation changes and it just works.

However, that's a lot of duplication you can't get rid of and sub-classing from flux::sequence_traits<std::vector<T>> won't help in this case, because the functions with calls to read_at needs to be shadowed by the derived class because they're not virtual methods, otherwise they will call the base version of read_at which then calls on .data() (immer::vector has no .data() because it's not contiguous).

virtual also doesn't seem to work in this case as the return type needs to be covariant and there's a lot of other limitations (no return type deduce, no templates, can't be static).

Relevant issue: P2279 We need a language mechanism for customization points

This is more of a "for your information" type of issue. I'm not aware of any good ways to make customization point easier to use. A workaround could be separating the trait into multiple different traits which the user can independently override, or offer a default implementation that looks for free functions which the user can provide, but that could make the customization point more confusing to use.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions