2

I am trying to create a variadic template function which would call a function to consecutive pairs of arguments.

The desired function signature would be:

template <typename ...Ts>
void apply(Ts &...args);

When called with apply(t1, t2, t3) the function should make a sequence of calls func(t1, t2)and func(t2, t3), where func is a function with signature:

template <typename L, typename R>
void func(L &left, R &right);

The order of operations is not really relevant in my context. The function has to be able to modify objects left and right, hence passed by reference. I cannot simply use polymorphic access through a base class pointer, since the objects have different class templates, a shared class cannot really be taken out.

Is it possible to achieve such a sequence of calls via a variadic template function? None of the pack expansion and fold expression examples that I've seen seem to cover such a scenario. Or should I pass my objects in a different fashion?

My initial attempt, included below (with some details omitted), packed all template parameters into a tuple, and then used a ‘const for-loop’ to ‘loop’ through the tuple elements. However, I soon came to realize that this approach would not work, because the lambda in the const-for loop invokes operator() const and therefore cannot modify the passed objects.

The code I was using does make the desired sequence of calls, but the objects are not modified (set_something() is not a const function). I had to resort to using a wrapper function with different numbers of template parameters, and making the calls to func manually.

template <std::size_t Begin, typename Callable, std::size_t... I>
constexpr void const_for_impl(Callable &&func, std::index_sequence<I...>) {
  (func(std::integral_constant<std::size_t, Begin + I>{}), ...);
}

template <std::size_t Begin, std::size_t End, typename Callable>
constexpr void const_for(Callable &&func) {
  const_for_impl<Begin>(std::forward<Callable>(func),
                        std::make_index_sequence<End - Begin>{});
};

template <typename... Ts>
void apply(Ts *... args) {
  auto tuple = std::make_tuple(std::forward<Ts>(args)...);

  const_for<0, sizeof...(args) - 1>(
      [&](auto I) { func((std::get<I>(tuple)), (std::get<I + 1>(tuple))); });
};

template <typename L, typename R>
void func(L &l, R &r) {
  // Validate with some type traits
  static_assert(has_some_property<L>::value);
  static_assert(has_another_property<R>::value);

  // Get a shared pointer to something common
  auto common = std::make_shared<typename something_common<L, R>::type>();

  l.set_something(common);
  r.set_something(common);
};

// Application scenario
int main() {

  ComplexObjectA<SomeType, SomeParameter> a;
  ComplexObjectB<AnotherType, AnotherParameter> b;
  ComplexObjectC c;

  apply(a, b, c);

  return 0;
}
Barry
  • 286,269
  • 29
  • 621
  • 977
Bruno K
  • 43
  • 6
  • 1
    After removing typos, you error is to use `make_tuple` instead of `forward_as_tuple` or `std::tie` [Demo](http://coliru.stacked-crooked.com/a/999ac6265d44c212), so you modification apply to a copy of the argument. – Jarod42 Jun 15 '18 at 15:22
  • @Jarod42 thanks for pointing that out, that does indeed seem to be the mistake I was making. I wasn't aware of `std::forward_as_tuple`, but I was aware of `std::tie`. For some reason I thought that the forwarding was achieving the same goal... My original approach does indeed work after changing `make_tuple` to `forward_as_tuple`. – Bruno K Jun 16 '18 at 12:35

2 Answers2

5

So, what's the problem? Simple fold-like template (and remember that template pattern-matching goes in reverse!)

template<typename T1, typename T2>
void apply(T1 &&t1, T2 &&t2) { func(t1, t2); }

template<typename T1, typename T2, typename... Ts>
void apply(T1 &&t1, T2 &&t2, Ts &&...ts) {
    func(t1, t2);
    return apply(t2, ts...);
}

Or, more precise, it should actually look as (thanks @MaxLanghof):

void apply(T1 &&t1, T2 &&t2) {
    func(std::forward<T1>(t1), std::forward<T2>(t2));
}

template<typename T1, typename T2, typename... Ts>
void apply(T1 &&t1, T2 &&t2, Ts &&...ts) {
    func(std::forward<T1>(t1), t2);

    return apply(std::forward<T2>(t2), std::forward<TS>(ts)...);
}
bipll
  • 11,747
  • 1
  • 18
  • 32
  • Should probably `std::forward<>` more but I guess it's easier to read right now. – Max Langhof Jun 15 '18 at 14:05
  • Sure, it's just I don't think we can forward `t2` twice. :( – bipll Jun 15 '18 at 14:12
  • 1
    @bipll Thanks a lot for your response, I think it provides the most straightforward answer to my primary question *"Is it possible to achieve such a sequence of calls via a variadic template function?"*. In the beginning I was trying to achieve my goal with this 'recursive' template function approach, but just couldn't figure out the right calls (I had the base case right, but I didn't think of having the main apply function with T1, T2, and ...Ts separately). There's still a lot I need to learn about templates! – Bruno K Jun 16 '18 at 12:41
  • Why is only `T1` forwarded in the first call to `func`? – Bruno K Jun 16 '18 at 12:58
  • 2
    @nurbo Because in general it is unknown in advance (to me, at least), whether `func()` keeps its second argument intact. If it does not steal its content or otherwise change it irrevocably (you can tell it having `func`'s source code), well, you can `std::forward(t2)` as well. In general, it is probably safe to pass it as a lvalue reference this time (it will be forwarded on the next recursion step). – bipll Jun 16 '18 at 16:17
2

An alternative (c++14) approach:

#include <utility>
#include <utility>
#include <tuple>
#include <iostream>

template <typename L, typename R>
void func(L &left, R &right) {
    std::cout << left << " " << right << std::endl;
}

template <typename Tup, std::size_t... Is>
void apply_impl(Tup&& tup, std::index_sequence<Is...>) {
    int dummy[] = { 0,  (static_cast<void>(func(std::get<Is>(tup), std::get<Is + 1>(tup))), 0)... };
    static_cast<void>(dummy);
}

template <typename ...Ts>
void apply(Ts &...args) {
    apply_impl(std::forward_as_tuple(args...), std::make_index_sequence<sizeof...(Ts) - 1>{});
}

int main() {
   int arr[] = {0, 1, 2, 3};
   apply(arr[0], arr[1], arr[2], arr[3]);
}

Output:

0 1
1 2
2 3

[online example]

To make it c++11 compliant one would need to use one of the available integer_sequence implementations.

W.F.
  • 13,888
  • 2
  • 34
  • 81
  • Thanks a lot for your answer, and for the online example! As @Jarod42 pointed out in his comment, the issue was that I wasn't using `forward_as_tuple`, like you did in your code. Your solution is quite concise. What is the purpose of the `int dummy[]` with the initializer list? – Bruno K Jun 16 '18 at 12:45
  • 1
    @nurbo its a trick that lets us use [fold expression](http://en.cppreference.com/w/cpp/language/fold) before c++17 – W.F. Jun 17 '18 at 19:44