2

I have a function transform that takes a tuple tp and some number of functions funcs as arguments, two indices Begin and End as template arguments. The function transform applies each of those functions from funcs to elements of tuple tp from std::get<Begin>(tp) upto (but not including) std::get<End>(tp) and collect the results into a result tuple and returns the tuple. Here is my code.

template <typename ... Rs, typename ... Ts, typename ... Fn, size_t ... Idx, size_t ... Left_Idx>
std::tuple<Rs ...> tranform_helper(std::index_sequence<Idx ...>, std::index_sequence<Left_Idx ...>,
                                   const std::tuple<Ts ...>& tp, Fn&& ... funcs) {
    return std::make_tuple<Rs ...>(std::forward<Fn>(funcs)(std::get<Idx>(tp)) ...,
                                   std::remove_reference_t<std::tuple_element_t<Left_Idx, std::tuple<Ts ...>>>(std::get<Left_Idx>(tp)) ...);
};

template <
        typename ... Rs,
        typename ... Ts,
        typename ... Fn,
        size_t Begin = 0,
        size_t End = std::index_sequence_for<Ts ...>().size()
>
std::tuple<Rs ...> transform(const std::tuple<Ts ...>& tp, Fn&& ... funcs) {
    constexpr size_t func_app_iters = std::min(End - Begin, std::index_sequence_for<Fn ...>().size());
    auto seq_idx = make_index_range<Begin, Begin + func_app_iters>();
    auto left_idx = make_index_range<Begin + func_app_iters, End>();
    return tranform_helper<Rs ...>(seq_idx, left_idx, tp, std::forward<Fn>(funcs) ...);
};

As far as I understand, only the trailing template parameters can be deduced. Since, in this function, the parameter Ts and Fn are always supposed to be deduced by the compiler, they must be put in the back. However, the parameter End has a default value depending on the parameter Ts and must be put after Ts, which would be problematic if I need to call transform with a specified value for End.

My question is: Is there any way to do template argument deduction in a way that is not sensitive to the order? If not, then is it possible to specify a template parameter with default value while making the compiler to deduce some parameters before that. That is:

auto record = std::make_tuple(102, "alice", 2000, false);
auto x = transform<int, std::string, int, bool, /*something like "Begin = 1, End = 3" */ >
(record, 
[](const char* name) {
    std::string cp(name); 
    cp[0] = static_cast<char>(toupper(cp[0])); 
    return cp;
}
[](const int& age) {
    return (age < 0 || age > 100) ? -1 : age;
}
);
std::cout << record << std::endl; // (102, "Alice", -1, false)

P.S. the actual implementation is a bit wrong in that it's only returning the elements between Begin and End but even I fix that, the ordering of the template parameter is still problematic.

charlieh_7
  • 334
  • 3
  • 12
  • Not entirely sure what you are asking but are you interested in a workaround? Maybe a "this is how I would do this" sort of thing? – Curious Jun 07 '17 at 09:28
  • Sure, One way I can think of would be use ssize_t and use -1 as default value. I'm sure there would be better workarounds. – charlieh_7 Jun 07 '17 at 09:31
  • Would passing the size arguments as function parameters do? If so why not pass them as the first two template parameters and if you want to retain the constexpr-ness then template the function on the first two parameters, and pass them as instances of `std::integral_constant` – Curious Jun 07 '17 at 09:36
  • @Curious I don't think so. The problem comes out of `Ts` and `End`. Eliminating `Fn` won't help. – charlieh_7 Jun 07 '17 at 09:39
  • I would let the compiler deduce `Rs` also, just make the return type of the function `auto` – Curious Jun 07 '17 at 09:40
  • @Curious I'm not sure what exactly you are saying. As far as I understand, constexpr could only come from template parameter. How can I get a constexpr out of a function parameter? Would you mind showing me some examples to clarify? – charlieh_7 Jun 07 '17 at 09:48
  • For example https://wandbox.org/permlink/MioZiA31QJQnvMYK – Curious Jun 07 '17 at 09:49
  • @Curious oh I see. I can actually use function parameter for default values. Thanks – charlieh_7 Jun 07 '17 at 09:55

1 Answers1

1

Yes, this is possible (C++17 solution to follow)

A bit difficult, but possible. I have a rough draft of a solution working here (I wanted to get something up because I have run out of time to hack at this problem).

Full code (explanation after):

template<int Index, int Begin, int End, int Size, class... Ts>
struct transform_apply;

template<int Index, int Begin, int End, class... Ts, class Fn, class... Fns>
struct transform_apply<Index, Begin, End, Index, std::tuple<Ts...>, Fn, Fns...>{
   static std::tuple<> apply(std::tuple<Ts...>,  Fn, Fns...){return{};}
};

template<int Index, int Begin, int End, class... Ts>
struct transform_apply<Index, Begin, End, Index, std::tuple<Ts...>>{
   static std::tuple<> apply(std::tuple<Ts...>){return{};}
};

template<int Index, int Begin, int End, int Size, class... Ts, class Fn, class... Fns>
struct transform_apply<Index, Begin, End, Size, std::tuple<Ts...>, Fn, Fns...>{
   static decltype(auto) apply(std::tuple<Ts...> tup, Fn f, Fns... fs)
   {
       if constexpr (Index >= Begin && Index < End)
       {
          auto val = f(std::get<Index>(tup));
          return std::tuple_cat(std::make_tuple(val), transform_apply<Index+1, Begin, End, Size, decltype(tup), Fns...>::apply(tup, fs...));
       }
       else
       {
          auto val = std::get<Index>(tup);
          return std::tuple_cat(std::make_tuple(val), transform_apply<Index+1, Begin, End, Size, decltype(tup), Fn, Fns...>::apply(tup, f, fs...));
       }
   };
};

template<int Index, int Begin, int End, int Size, class... Ts>
struct transform_apply<Index, Begin, End, Size, std::tuple<Ts...>>{
   static decltype(auto) apply(std::tuple<Ts...> tup)
   {
        auto val = std::get<Index>(tup);
        return std::tuple_cat(std::make_tuple(val), transform_apply<Index+1, Begin, End, Size, decltype(tup)>::apply(tup));
   };
};

template<int Begin, int End, class... Ts, class... Fns>
decltype(auto) transform(std::tuple<Ts...> tup, Fns... fns)
{
    return transform_apply<0, Begin, End, sizeof...(Ts), decltype(tup), Fns...>::apply(tup, fns...);
}

Explanation

We essentially need to iterate over the tuple, and if our index is within the specified bounds, apply a function, else do not apply a function. The result of that function is pre-pended as a tuple to the result of the next call (tuple_cat, you have saved me).

Taking advantage of C++17's constexpr if makes this checking really nice.

The code above iterates recursively, with a specialization for when our index is the same as the size of the tuple (end iteration). Note that we must continue iterating even after the specified "end" so that we can obtain the rest of the tuple.

This code probably makes too many copies (like I said it's not a production-quality solution).

auto record = std::make_tuple(102, "alice", 2000, false);
auto nameToUpper = [](const char* name) {
    std::string cp(name); 
    cp[0] = static_cast<char>(toupper(cp[0])); 
    return cp;
};
auto ageTest = [](const int& age) {
    return (age < 0 || age > 100) ? -1 : age;
};
auto transformed_record = transform<1, 3>
(   record, 
    nameToUpper,
    ageTest
);
print(record);
print(transformed_record);

Output

(I used my own answer for how to print a tuple to implement print):

(102, alice, 2000, 0)
(102, Alice, -1, 0)
AndyG
  • 39,700
  • 8
  • 109
  • 143
  • Excellent answer. I get inspired by your answer and hacked together [a similar solution](https://wandbox.org/permlink/lufoJwUxOv7Hgies). It seems function overloading is enough and there is no need for actual template partial specialization, and I used [index sequences](https://codereview.stackexchange.com/questions/59059/compile-time-fixed-templated-integer-range) rather than recursions. Although I'm learning FP, doing recursions using TMP seems too intense for me :P. Probably not as efficient though. Thank you very much. – charlieh_7 Jun 12 '17 at 06:50
  • A quick question: Should the function parameters be passed by reference or by value? Should it be perfect-forwarded? I'm not sure what are the implications, especially on the captured variables. – charlieh_7 Jun 12 '17 at 06:54
  • @charlieh_7 probably by reference, as you'd get a ref from std::get on the tuple anyway – AndyG Jun 12 '17 at 10:07
  • Sorry I mean the lambda parameters (fns) – charlieh_7 Jun 12 '17 at 10:41
  • @charlieh_7: Perfect forwarding would probably be best. Also, it wouldn't hurt to be perfect forwarding all the arguments anyway, in retrospect. – AndyG Jun 12 '17 at 11:38
  • @charlieh_7: I was able to take a look at your code. Really clever stuff. One note, though: you seem to be over-using `index_sequence_for::size` when you could use `sizeof...()` instead. [Here's a fixed version](https://wandbox.org/permlink/HKZyoFDkvjKzPWOj) – AndyG Jun 12 '17 at 12:12
  • yeah just found out about size... Thx – charlieh_7 Jun 13 '17 at 03:44
  • @charlieh_7 How does this answer the question? Wasn't the main issue that you wanted default values for the indices parameters? As far as I can see, this is not achieved by this answer. – Daniel Jour Jun 18 '17 at 22:20