0

When dealing with generic structures to hold some combination of other entities I often end up using std::tuple with the need to apply operations to individual elements of the std::tuple. For example, when I implemented a "zip iterator" the underlying ranges and iterators were stored in a std::tuple. While it is reasonably straight forward to hack up custom function or class templates using an std::index_sequence<std::tuple_size<Tuple>> to get the relevant elements, it seems using algorithms operating on std::tuples or std::tuple-like structures (e.g., std::array or std::pair) could improve the code.

Some operations are fairly straight forward to implement. For example, tuple_for_each() could either dispatch to a recursive implementation or an implementation based on std::index_sequence to apply the elements:

template <typename Tuple, typename Fun>
void tuple_for_each(Tuple&& tuple, Fun fun)
{
    auto const impl = [&tuple, fun]<std::size_t...I>(std::index_sequence<I...>){
        (fun(std::get<I>(tuple)), ...);
    };
    impl(std::make_index_sequence<std::tuple_size_v<std::decay_t<Tuple>>>());
}

When the operation isn't really element-wise as in for_each or transform (for the latter producing the return type is a bit more interesting) but the result of a function applied to the elements is combined to produce a result a generic algorithm doesn't seem to work as simple. The algorithm would be somewhat like accumulate or inner_product. These can be written quite easy in a custom setting, e.g.,

template <typename... T>
struct some_struct {
    std::tuple<T...> tuple;

    template <typename Tuple, std::size_t... I>
    static bool equals(Tuple&& t0, Tuple&& t1, std::index_sequence<I...>) {
        return ((std::get<I>(t0) == std::get<I>(t1)) && ...);
    }
    bool operator== (some_struct const& other) const {
        return equals(this->tuple, other.tuple, std::make_index_sequence<sizeof...(T)>());
    }
};

It would be nice if this operator==() could just delegate to a suitable tuple algorithm:

bool operator== (some_struct const& other) const  {
    return tuple_inner_product(this->tuple, other.tuple, true, std::equal_to<>(), std::logical_and<>());
}

There doesn't seem to be a way to implement tuple_inner_product with a fold expression other than creating special versions based on the type of the last argument. I know how to implement a recursive version but I don't think that version would short-circuit the evaluation if the combining operation is one of the logical operands (the function arguments always need to be determined before the function can be called):

template <typename T0, typename T1, typename Init, typename Transform, typename Combine>
auto tuple_inner_product(T0&& t0, T1&& t1, Init init, Transform transform, Combine combine) {
    auto const recurse = [&t0, &t1, transform, combine]<std::size_t I>(
                               std::integral_constant<std::size_t, I>,
                               auto const& r, auto init) {
        if constexpr (I == std::min(std::tuple_size_v<std::decay_t<T0>>,
                                    std::tuple_size_v<std::decay_t<T1>>)) {
            return init;
        }
        else {
            return combine(transform(std::get<I>(t0), std::get<I>(t1)),
                           r(std::integral_constant<std::size_t, I+1>(), r, init));
        }
    };
    return recurse(std::integral_constant<std::size_t, 0u>(), recurse, init);
}

Thus, the question becomes whether there is a way to implement this algorithm using fold expressions?

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • Have you seen std::apply? Both of your examples look easy to write with it. (Not able to actually write and test it right now.) – HTNW Dec 26 '20 at 21:56
  • 1
    You want [this](https://stackoverflow.com/q/27582862/2069064) basically. – Barry Dec 26 '20 at 22:04
  • @HTNW: I think `std::apply()` is sort of the other way around: it calls a function expanding the `std::tuple` elements to become the function arguments. I want to, well, _apply_ a function to each of the elements. For example, if the `std::tuple` contains iterators for different sequences something like `tuple_for_each(tuple, [](auto& it){ ++it; })` becoming `++std::get<0>(tuple), ++std::get<1>(tuple), ...`. – Dietmar Kühl Dec 26 '20 at 22:04
  • @Barry: "fold expression with arbitrary callable": yes, mostly that's it. Thanks! I guess, the moment I hide the logic operator into a function object I loose the short circuit property so maybe it is an unreasonable expectation to hope to get it back (it could be recovered for known types like `std::logical_and<>()`, though, by having a special implementation for these). – Dietmar Kühl Dec 26 '20 at 22:11
  • 2
    Follow up of HTNW comment: `std::apply([](auto&... its){(++its, ...);}, tuple);`. – Jarod42 Dec 26 '20 at 22:35
  • @Jarod42: thanks for clarifying! I didn't see this approach but that, indeed, works for my `for_each` and probably also for `transform`. – Dietmar Kühl Dec 26 '20 at 22:41
  • 1
    @DietmarKühl Here we go: `void tuple_for_each(auto &&t, auto f) { apply([&](auto&&... xs) { (f(forward(xs)), ...); }, forward(t)); } auto tuple_transform(auto &&t, auto f) { return apply([&](auto&&... xs) { return make_tuple(f(std::forward(xs))...); }, std::forward(t)); } bool tuple_eq(auto &&l, auto &&r) { return apply([&](auto&&... ls) { return apply([&](auto&&... rs) { return ((std::forward(ls) == std::forward(rs)) && ...); }, forward(r)); }, forward(l)); }` – HTNW Dec 26 '20 at 22:52
  • @HTNW: thanks! Jarod42 also enlightened me to see how to use `std::apply()` to implement these functions! I didn't realize this neat trick. – Dietmar Kühl Dec 26 '20 at 22:55

1 Answers1

1

I'd start with code that generates a tuple of integral constants.

template<TupleLike Tuple>
constexpr auto tuple_indexes(Tuple&&);

Which is std::tuple<std::integral_constant<std::size_t,0>, std::integral_constant<std::size_t,1>, ..., std::integral_constant<std::size_t,N-1> for an nary tuple.

Now we get:

bool operator== (some_struct const& other) const  { 
  return std::apply([&](auto...Is){
    return (true &&... && (std::get<Is>(this->tuple)==std::get<Is>(other.tuple)));
  }, tuple_indexes(this->tuple));
}

A vocabulary function I also find useful is

template<std::size_t N>
using index_t=std::integral_constant<std::size_t,N>;
template<std::size_t N>
constexpr index_t<N> index={};

template<std::size_t...Is>
auto indexer_for(std::index_sequence<Is...>){
  return [](auto&& f)->decltype(auto){ return f(index<Is>...); };
}
template<std::size_t N>
auto indexer_upto(){
  return indexer_for(std::make_index_sequence<N>{});
}

this can be used many ways. For example,

bool operator== (some_struct const& other) const  { 
  return indexer_upto<sizeof...Ts>()([&](auto...Is){
    return (true &&... && (std::get<Is>(this->tuple)==std::get<Is>(other.tuple)));
  });
}

as more efficient and more generic than my tuple of indexes above.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • A function creating an `std::index_sequence` from a `std::tuple` is a good idea (although I think it will require slightly different syntax) to avoid a lot of messing about otherwise - thanks! – Dietmar Kühl Dec 26 '20 at 22:36
  • @diet you need a way to expand the index sequence into indexes; apply is a prewritten one, or you can roll your own. I write `indexer(index_sequence)(lambda)` for it for example. – Yakk - Adam Nevraumont Dec 26 '20 at 23:15
  • Thanks for the update: I also didn't see using `integral_constant` here and that converts into `constexpr` values. That's cool! With that my question is actually answered [unlike with the claimed duplicate]. – Dietmar Kühl Dec 26 '20 at 23:24
  • @diet another version added. Tuple of indexes is compile time expensive. – Yakk - Adam Nevraumont Dec 26 '20 at 23:33