最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

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

programmeradmin3浏览0评论

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 c++20.

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?

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 c++20.

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?

Share Improve this question edited Mar 16 at 0:14 JeJo 33.5k7 gold badges55 silver badges95 bronze badges asked Mar 15 at 22:00 jackjack 1,8021 gold badge9 silver badges22 bronze badges 2
  • How about template <std::size_t N>std::array<interval, N> split(std:array<std::string_view, N> names, std::array<float, N - 1> splitters, bool flag = true) const;? – Jarod42 Commented Mar 17 at 9:06
  • @Jarod42 I find it a lot easier to read at callsite if the arguments are supplied separately and in logical order (aka alternating). Implementation-wise, your idea is definitely more straight-forward though. – jack Commented Mar 17 at 19:10
Add a comment  | 

2 Answers 2

Reset to default 4

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

You might able to combine the split function using some immediately invoking template lambda as follows:

template<typename... Args>
static constexpr bool check_args_alternating() noexcept
{
    constexpr size_t N = sizeof...(Args);
    if constexpr (N % 2 == 0 || N < 3)   return false;
    return []<std::size_t... Is>(std::index_sequence<Is...>) {
        return ((Is % 2 == 0
            ? std::is_convertible_v<std::decay_t<std::tuple_element_t<Is, std::tuple<Args...>>>, std::string_view>
            : std::is_convertible_v<std::decay_t<std::tuple_element_t<Is, std::tuple<Args...>>>, float>
            ) && ...);
    }(std::make_index_sequence<N>{});
}

template<typename... Args> requires (check_args_alternating<Args...>())
constexpr auto split(Args&&... args) const
{
   auto tup = std::forward_as_tuple(std::forward<Args>(args)...);
   constexpr size_t count = (sizeof...(Args) + 1) / 2;

   // Create indices for our array and use them to directly construct the array
   return[this, &tup]<std::size_t... Is>(std::index_sequence<Is...>) {
      return std::array<interval, count> {
         (([this, &tup]() -> interval {
            if constexpr (Is == 0)
               return split_off_chunk(std::get<0>(tup), 0.0f, std::get<1>(tup));
            else if constexpr (Is == count - 1)
               return split_off_chunk(std::get<2 * Is>(tup), std::get<2 * Is - 1>(tup), 1.0f);
            else
               return split_off_chunk(std::get<2 * Is>(tup), std::get<2 * Is - 1>(tup), std::get<2 * Is + 1>(tup));
            })())...
      };
   }(std::make_index_sequence<count>{});
}

where check_args_alternating checks the requires an odd number of arguments as well as alternating string-like and float arguments.

((See live demo))

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

发布评论

评论列表(0)

  1. 暂无评论