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
andforNArgsStep
? - Is there any way of avoiding the creation of a tuple? Can the arguments be forwarded in a better way?
1 Answer 1
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{});
}
Explore related questions
See similar questions with these tags.