c++templatesc++20variadic-templatesfunction-templates

How to split an interval into an arbitrary number of sub-intervals using variadic templates?


I want to split a custom interval class into smaller sub-intervals. The number of sub-intervals depends on the number of supplied arguments.

An example implementation (compiler-explorer link) with separate functions for the first two special cases (e.g. split into two or three sub-intervals):

class interval
{
public:
    interval(std::string_view name, float start, float end)
        : name(name)
        , start(start)
        , end(end) 
    {
        if (start >= end)
            throw std::invalid_argument("`start` must be smaller than `end`!");
    }

    interval split_off_chunk(std::string_view name, float relStart,
        float relEnd) const
    {
        if (!((0 <= relStart) && (relStart < relEnd) && (relEnd <= 1)))
            throw std::invalid_argument(
                "`relStart`, `relEnd` must be relative values, e.g. `0<=relStart < "
                "relEnd <= 1`!");

        const float len = end - start;
        const float childStart = (relStart == 0) ? start : start + relStart * len;
        const float childEnd = (relEnd == 1) ? end : start + relEnd * len;
        return { name, childStart, childEnd };
    }

    std::array<interval, 2> split(std::string_view name0, float relSplit01,
        std::string_view name1) const
    {
        if (!((0 < relSplit01) && (relSplit01 < 1)))
            throw std::invalid_argument(
                "`relSplit01` must be a relative value, e.g. `0 < relSplit01 < "
                "1`!");

        return { split_off_chunk(name0, 0, relSplit01),
                split_off_chunk(name1, relSplit01, 1) };
    }

    std::array<interval, 3> split(std::string_view name0, float relSplit01,
        std::string_view name1, float relSplit12,
        std::string_view name2) const 
    {
        if (!((0 < relSplit01) && (relSplit01 < relSplit12) && (relSplit12 < 1)))
            throw std::invalid_argument(
                "`relSplit` must be relative values, e.g. `0 < relSplit01 < "
                "relSplit12 < 1`!");

        return { split_off_chunk(name0, 0, relSplit01),
                split_off_chunk(name1, relSplit01, relSplit12),
                split_off_chunk(name2, relSplit12, 1) };
    }

    void print() const { std::print("name: {}. [{}, {}]\n", name, start, end); }

private:
    std::string name;
    float start;
    float end;
};

I am looking for one function that can handle arbitrary number of sub-intervals. It also needs to work with .

I tried something along the lines of

template<class... Args>
consteval bool CheckArgs() {
    if (sizeof...(Args) % 2 != 1)
        return false;
    if (sizeof...(Args) < 3)
        return false;
    // howto check alternating string_view, float ???
}

template<class... Args>
consteval size_t GetCount() { return (sizeof...(Args)+1)/2; }

template<class... Args>
    requires (CheckArgs<Args>())
std::array<interval, GetCount<Args...>()> split(Args... args) {
    // howto extract the names and relSplit floats?
    // howto create the array and initialize with the proper arguments?
}

It should be fairly efficient, e.g. no unnecessary intermediate vector objects.


Bonus 1 Is it possible to have a trailing boolean argument that has a default value? E.g.

class interval {
 public:
  interval(std::string_view name, float start, float end, bool flag = true);

  interval split_off_chunk(std::string_view name, float relStart, float relEnd,
                           bool flag = true) const {
    // ...
    return {name, childStart, childEnd, flag};
  }

  std::array<interval, 2> split(std::string_view name0, float relSplit01,
                                std::string_view name1, bool flag = true) const {
    // ...                                
    return {split_off_chunk(name0, 0, relSplit01, flag),
            split_off_chunk(name1, relSplit01, 1, flag)};
  }

  std::array<interval, 3> split(std::string_view name0, float relSplit01,
                                std::string_view name1, float relSplit12,
                                std::string_view name2, bool flag = true) const;
  // ...                            
  bool flag;
};

Bonus 2 Currently, the sub-intervals always start at 0 and end at 1. Can that be optionally adjusted? E.g. the variadic template argument Args be checked if its first type is a float, if so then use that instead of the default 0 value, resp. for the last value?


Solution

  • You could make split take a variable amount of arguments to make it more generic.

    This doesn't require interval to be default constructible and it also allows only one argument to split (which gets the full range):

    template <std::size_t I, class T>
    interval get_chunk(T& tup) const {
        float begin = 0.f, end = 1.f;
        if constexpr (I != 0) {                             // not the first
            static_assert(
                std::convertible_to<std::tuple_element_t<I * 2 - 1, T>, float>);
            begin = std::get<I * 2 - 1>(tup);
        }
        if constexpr (I * 2 + 1 != std::tuple_size_v<T>) {  // not the last
            static_assert(
                std::convertible_to<std::tuple_element_t<I * 2 + 1, T>, float>);           
            end = std::get<I * 2 + 1>(tup);
        }
        static_assert(std::convertible_to<std::tuple_element_t<I * 2, T>,
                                            std::string_view>);
        return split_off_chunk(std::get<I * 2>(tup), begin, end);
    }
    
    template <class... Args>
    auto split(Args&&... args) const {
        static_assert(sizeof...(Args) % 2 == 1, "wrong number of arguments");
    
        auto tup = std::forward_as_tuple(std::forward<Args>(args)...);
    
        return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
            return std::array{get_chunk<Is>(tup)...};
        }(std::make_index_sequence<sizeof...(Args) / 2 + 1>{});
    }
    

    Demo