5
\$\begingroup\$

Interactive Wandbox example

Inspired by this Stack Oveflow question, I've implemented a function that, when called with a desired arity, a callable object, and a variadic amount of arguments, calls the passed callable object forwarding the passed arguments in groups of the desired arity.

Example:

auto printAll([](auto... xy){ (std::cout << ... << xy); std::cout << " "; });
forNArgs<2> // `2` is the desired arity
(
 // `printAll` is the callable object.
 printAll, 
 // Variadic arguments.
 0, 1, 2, 3, 4, 5, 6, 7
);
// ...will result in code equivalent to...
printAll(0, 1);
printAll(2, 3);
printAll(4, 5);
printAll(6, 7);

I'm trying to simplify the original code using fold expressions. This is what I have so far:

// "Third step": call `mFn` once getting the correct elements from the forwarded tuple.
template<std::size_t TIStart, typename TF, typename TTpl, std::size_t... TIs>
void forNArgsStep(TF&& mFn, TTpl&& mTpl, std::index_sequence<TIs...>)
{
 mFn(std::get<TIStart + TIs>(mTpl)...);
}
// "Second step": use a comma operator fold expression to call the "third step" `numberOfArgs / TArity` times.
template<std::size_t TArity, typename TF, typename TTpl, std::size_t... TIs>
void forNArgsExpansion(TF&& mFn, TTpl&& mTpl, std::index_sequence<TIs...>)
{
 using SeqGet = std::make_index_sequence<TArity>;
 (forNArgsStep<TIs * TArity>(mFn, mTpl, SeqGet{}), ...);
}
// "First step / interface".
template<std::size_t TArity, typename TF, typename... Ts>
void forNArgs(TF&& mFn, Ts&&... mXs)
{
 constexpr auto numberOfArgs(sizeof...(Ts));
 static_assert(numberOfArgs % TArity == 0,
 "Invalid number of arguments");
 auto&& asTpl(std::forward_as_tuple(std::forward<Ts>(mXs)...));
 // We need to "convert" the `Ts...` pack into a "list" of packs
 // of size `TArity`.
 using SeqCalls = std::make_index_sequence<numberOfArgs / TArity>;
 forNArgsExpansion<TArity>(mFn, asTpl, SeqCalls{});
}

More examples:

int main()
{
 // Prints "01 23 45 67":
 forNArgs<2>
 (
 [](auto... xy){ (std::cout << ... << xy); std::cout << " "; },
 0, 1, /**/ 2, 3, /**/ 4, 5, /**/ 6, 7
 );
 std::cout << "\n";
 // Prints "abc def ghi":
 forNArgs<3>
 (
 [](auto... xyz){ (std::cout << ... << xyz); std::cout << " "; },
 "a", "b", "c", /**/ "d", "e", "f", /**/ "g", "h", "i"
 );
 std::cout << "\n";
}

I think my perfect forwarding is incorrect.

  • When should I forward the tuple?
  • When should I forward the function?

How can I simplify the code further?

  • Is there any way of avoiding the two extra helper functions forNArgsExpansion and forNArgsStep?
  • Is there any way of avoiding the creation of a tuple? Can the arguments be forwarded in a better way?
asked Sep 6, 2015 at 16:23
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I think my perfect forwarding is incorrect.

I think so too. All three functions accept mFn as a forwarding reference, but never attempt to forward it. The top-level forNArgs() can certainly do so, but the forwarding needs to stop there because forNArgsExpansion() uses it repeatedly.

asTpl is also a forwarding reference, but we don't forward it. As this is just a tuple of references, I think we should move it:

 auto asTpl(std::forward_as_tuple(std::forward<Ts>(mXs)...));
 forNArgsExpansion<TArity>(mFn, std::move(asTpl), SeqCalls{});

I recommend using std::invoke() on mFn rather than calling its operator() directly - that's a general good practice for invokables.

I think it helps to test with a variety of types. Here's some that stretch the boundaries of what I believe we intend to support:

#include <ostream>
class moveonly_string
{
 std::string s;
public:
 moveonly_string(std::string s)
 : s{std::move(s)}
 {}
 moveonly_string(const moveonly_string&) = delete;
 moveonly_string(moveonly_string&&) = default;
 moveonly_string& operator=(const moveonly_string&) = delete;
 moveonly_string& operator=(moveonly_string&&) = default;
 friend std::ostream& operator<<(std::ostream& os, const moveonly_string& s)
 {
 return os << s.s;
 }
};
moveonly_string operator""_mo(const char *s, unsigned long len) {
 return {{s,len}};
}
class immovable_string
{
 std::string s;
public:
 immovable_string(std::string s)
 : s{std::move(s)}
 {}
 immovable_string(const immovable_string&) = delete;
 immovable_string& operator=(const immovable_string&) = delete;
 friend std::ostream& operator<<(std::ostream& os, const immovable_string& s)
 {
 return os << s.s;
 }
};
immovable_string operator""_im(const char *s, unsigned long len) {
 return {{s,len}};
}
 // Test with move-only strings
 // Prints "abc def ghi":
 forNArgs<3> ([](auto... xyz) {
 (std::cout << ... << xyz);
 std::cout << ' ';
 },
 "a"_mo, "b"_mo, "c"_mo,
 "d"_mo, "e"_mo, "f"_mo,
 "g"_mo, "h"_mo, "i"_mo
 );
 std::cout << '\n';
 // Immovable strings; non-const function
 // Prints "abc 1 def 2 ghi 3":
 forNArgs<3>([i = 0](auto const&... xyz) mutable {
 (std::cout << ... << xyz);
 std::cout << ' ' << ++i << ' ';
 },
 "a"_im, "b"_im, "c"_im,
 "d"_im, "e"_im, "f"_im,
 "g"_im, "h"_im, "i"_im
 );
 std::cout << '\n';

I had to make a few small tweaks to argument types and moving/forwarding for these to all succeed:

#include <functional>
#include <tuple>
#include <utility>
// "Third step": call `mFn` once getting the correct elements from the
// forwarded tuple.
template<std::size_t TIStart, typename TF, typename TTpl, std::size_t... TIs>
void forNArgsStep(TF&& mFn, TTpl& mTpl, const std::index_sequence<TIs...>)
{
 std::invoke(mFn, std::move(std::get<TIStart + TIs>(mTpl))...);
}
// "Second step": use a comma operator fold expression to call the
// '"third step" `numberOfArgs / TArity` times.
template<std::size_t TArity, typename TF, typename TTpl, std::size_t... TIs>
void forNArgsExpansion(TF&& mFn, TTpl mTpl, const std::index_sequence<TIs...>)
{
 using SeqGet = std::make_index_sequence<TArity>;
 (forNArgsStep<TIs * TArity>(mFn, mTpl, SeqGet{}), ...);
}
// "First step / interface".
template<std::size_t TArity, typename TF, typename... Ts>
void forNArgs(TF&& mFn, Ts&&... mXs)
{
 constexpr auto numberOfArgs(sizeof...(Ts));
 static_assert(numberOfArgs % TArity == 0,
 "Invalid number of arguments");
 auto asTpl(std::forward_as_tuple(std::forward<Ts>(mXs)...));
 // We need to "convert" the `Ts...` pack into a "list" of packs
 // of size `TArity`.
 using SeqCalls = std::make_index_sequence<numberOfArgs / TArity>;
 forNArgsExpansion<TArity>(std::forward<TF>(mFn), std::move(asTpl), SeqCalls{});
}
answered Sep 15, 2024 at 15:43
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.