I'm implementing apply_each
for tuple-like object as a general function which may be a combined version of for_each
and tuple_transform
in some implementations for tuple iteration.
As the name suggests, the first argument which is invocable will apply each element of the tuple.
Function Prototype of apply_each
:
template <typename F, typename... Tuples>
constexpr decltype(auto) apply_each(F&& f, Tuples&&... ts);
As usual, you can iterate over the tuple
std::tuple tup_1 {1, 2.5, "three", 'F'};
gold::apply_each([](const auto& elem) {
std::cout << elem << ' ';
}, tup_1);
Output:
1 2.5 three F
Tuple-like object can also be used
gold::apply_each([](const auto& elem) {
std::cout << elem << ' ';
}, std::array{1, 2, 3, 4});
Output:
1 2 3 4
How about two or more tuples? Sure! No problem
The function apply_each
takes the first parameter that is callable and the arity should match with the number of elements from the tuple. There is internally a zipper
function that zips the function so that the tuples that are zipped can be traversed together.
You don't need to worry about the length of every tuple because it automatically selects the minimum size of the tuples and truncates it.
std::tuple tup_1 {1, 2, 3, 4};
std::tuple tup_2 {1, 2, 3, 4, 5};
std::tuple tup_3 {1, 2, 3};
gold::apply_each([i = 1](const auto&... args) mutable {
if (i == 1) std::cout << "Taking the sum of each element..." << '\n';
std::cout << "Iteration " << i++ << ": " << (args + ...) << '\n';
}, tup_1, tup_2, tup_3);
Output:
Taking the sum of each element...
Iteration 1: 3
Iteration 2: 6
Iteration 3: 9
Well you can still use the parameter list: const auto& arg1, const auto& arg2, const auto& arg3
.
What about the return type? Sure!
std::tuple tup_1 {1, 2, 3, 4};
std::tuple tup_2 {1, 2, 3, 4, 5};
std::tuple tup_3 {1, 2, 3};
auto sum_tuple = gold::apply_each([](const auto&... args) mutable {
return (args + ...);
}, tup_1, tup_2, tup_3);
static_assert(std::is_same_v<std::tuple<int, int, int>, decltype(sum_tuple)>); // passed!
gold::apply_each([i = 1](const auto& elem) mutable {
std::cout << "Sum " << i++ << ": " << elem << '\n';
}, sum_tuple);
Output:
Sum 1: 3
Sum 2: 6
Sum 3: 9
What about mutating them? Hmmm...
Well...
std::tuple<int, double> tup_1;
gold::apply_each([](auto& elem){
if constexpr (std::is_integral_v<std::decay_t<decltype(elem)>>) {
elem = 3;
} else {
elem = 3.14159;
}
}, tup_1);
gold::apply_each([](const auto& elem){
std::cout << elem << ' ';
}, tup_1);
It seems to be compiled, but! The tuple doesn't change...
0 0
Why? Well, it's difficult for me to make communicate with references when that invocable object is passed, and I can't figure out easily the argument type of the function because it may be a template or not. So... I used a reference_wrapper
!, because that's the only one who saved me.
Let's see...
gold::apply_each([](auto& elem){
if constexpr (std::is_integral_v<std::decay_t<decltype(elem)>>) {
elem = 3;
} else {
elem = 3.14159;
}
}, std::ref(tup_1));
Oops!! It seems the compiler is very angry. Well, the elem
is still actually a reference_wrapper
so the error is about operator=
mismatch, and is not converted into primitive reference because it is a generic lambda.
So, I find another solution and overload
is the key!
gold::apply_each(gold::overload(
[](int& elem){ elem = 3; },
[](double& elem) { elem = 3.14159; }
), std::ref(tup_1));
The compiler seems very happy now :)
A reference wrapper is implicitly converted into reference type because the argument type is known which is int&
and double&
What is the other way for passing a generic lambda? So I created a struct overload_unref
!
gold::apply_each(gold::overload_unref(
[](auto& elem) {
if constexpr (std::is_integral_v<std::decay_t<decltype(elem)>>) {
elem = 3;
} else {
elem = 3.14159;
}
}
), std::ref(tup_1));
The mechanics of overload_unref
is that it will unwrap the argument if the type is reference_wrapper
.
Here is the full implementation of apply_each
together with the helper tuple_zip
function:
namespace gold {
namespace detail {
template <typename>
struct is_ref_wrapper_ : std::false_type {};
template <typename T>
struct is_ref_wrapper_<std::reference_wrapper<T>> : std::true_type {};
template <typename T>
inline constexpr bool is_ref_wrapper_v_ = is_ref_wrapper_<T>::value;
template <typename T>
struct maybe_unwrap_ref_wrapper_ {
using type = std::conditional_t<is_ref_wrapper_v_<T>, std::unwrap_reference_t<T>, T>;
};
template <typename T>
using maybe_unwrap_ref_wrapper_t_ = typename maybe_unwrap_ref_wrapper_<T>::type;
template <typename T>
using get_ref_wrap_type_t_ = typename T::type;
template <std::size_t I, typename... Tuple>
using zip_tuple_at_index_t_ = std::tuple<
std::conditional_t<
is_ref_wrapper_v_<std::decay_t<Tuple>>,
std::reference_wrapper<
std::conditional_t<
std::is_const_v<std::experimental::detected_or_t<Tuple, get_ref_wrap_type_t_, Tuple>>,
const std::tuple_element_t<I, std::decay_t<maybe_unwrap_ref_wrapper_t_<Tuple>>>,
std::tuple_element_t<I, std::decay_t<maybe_unwrap_ref_wrapper_t_<Tuple>>>
>
>,
std::tuple_element_t<I, std::decay_t<maybe_unwrap_ref_wrapper_t_<Tuple>>>
>...
>;
template <std::size_t I, typename... Tuple>
constexpr zip_tuple_at_index_t_<I, Tuple...> zip_tuple_at_index_(Tuple&&... ts) {
return { []<typename T>(T&& t) {
if constexpr (is_ref_wrapper_v_<std::remove_cvref_t<T>>) {
using type_ = std::remove_reference_t<typename T::type>; // reference_wrapper<T>::type
if constexpr (std::is_const_v<type_>)
return std::cref(std::get<I>(std::forward<T>(t).get()));
else
return std::ref(std::get<I>(std::forward<T>(t).get()));
} else
return std::get<I>(std::forward<T>(t));
}(std::forward<Tuple>(ts)) ... };
}
template <typename... Tuple, std::size_t... Is>
constexpr std::tuple<zip_tuple_at_index_t_<Is, Tuple...>...>
tuple_zip_impl_(std::index_sequence<Is...>, Tuple&&... ts) {
return { zip_tuple_at_index_<Is>(std::forward<Tuple>(ts)...)... };
}
} // namespace detail
// tuple_zip
template <typename... Tuple> requires (sizeof...(Tuple) > 0)
constexpr decltype(auto) tuple_zip(Tuple&&... ts) {
constexpr auto min_size_ = std::ranges::min({
std::tuple_size_v<std::decay_t<detail::maybe_unwrap_ref_wrapper_t_<Tuple>>>...
});
return detail::tuple_zip_impl_(
std::make_index_sequence<min_size_>{},
std::forward<Tuple>(ts)...
);
}
namespace detail {
template <typename F, typename... Tuple, std::size_t... Is> requires (sizeof...(Tuple) > 0)
constexpr decltype(auto) apply_each_impl_(std::index_sequence<Is...>, F&& f, Tuple&&... ts) {
decltype(auto) zipped_ = tuple_zip(std::forward<Tuple>(ts)...);
using result_t_ = decltype([&]{
return (std::apply(std::forward<F>(f), std::get<Is>(zipped_)), ...);
}());
if constexpr (std::is_void_v<result_t_>) {
return (
std::apply(std::forward<F>(f), std::get<Is>(zipped_)), ...
);
} else {
return std::make_tuple(
std::apply(
std::forward<F>(f), std::get<Is>(zipped_)
)...
);
}
}
} // namespace detail
// apply_each
template <typename F, typename... Tuple>
constexpr decltype(auto) apply_each(F&& f, Tuple&&... ts) {
using indices_ = std::make_index_sequence<std::ranges::min({
std::tuple_size_v<std::decay_t<detail::maybe_unwrap_ref_wrapper_t_<Tuple>>>...
})>;
return detail::apply_each_impl_(indices_{}, std::forward<F>(f), std::forward<Tuple>(ts)...);
}
// unref
template <typename T>
constexpr T&& unref(T&& t) {
return std::forward<T>(t);
}
template <typename T>
constexpr T& unref(std::reference_wrapper<T> t) {
return t.get();
}
// overload
template <typename... Fs>
struct overload : Fs... {
using Fs::operator()...;
};
// I have to provide this, otherwise, I would get a compilation error that I can't explain further...
template <typename... Fs>
overload(Fs...) -> overload<Fs...>;
namespace requirements {
template <typename... Ts>
concept has_at_least_ref_wrapper_ = (detail::is_ref_wrapper_v_<std::remove_cvref_t<Ts>> || ...);
} // namespace requirements
// overload_unref
template <typename... Fs>
struct overload_unref : overload<Fs...> {
overload_unref(Fs&&... fs)
: overload<Fs...>{ std::forward<Fs>(fs) ... } {}
using overload<Fs...>::operator();
template <typename... Ts>
requires requirements::has_at_least_ref_wrapper_<Ts...>
constexpr auto operator()(Ts&&... args) {
return (*this)(unref(std::forward<Ts>(args))...);
}
};
} // namespace gold
Current problems:
- The definition of alias
zip_tuple_at_index_t_
is over complicated even though I'm the one who wrote this but I can't further simplify this one. - Same to
zip_tuple_at_index_
. - I don't know if it's safe to use
std::experimental::detected_or_t
even if it's experimental. - I'm writing
decltype(auto)
for no reason but I don't know if that affects the value category of the type. - There will be a potential compiler error for GCC 11 maybe.
Demo: https://gcc.godbolt.org/z/hz94faWad
- Another problem is that, the code will be compiled fine in GCC 10.3, but not in GCC 11 higher.
1 Answer 1
I see two main issues with the design.
- That you need to use
std::ref
to operate on the tuples themselves and not their copies. This isn't good. It is used this way inthread
and similar cases but there it creates an object and knowledge whether a copy is needed to be a kept or a reference is extremely important. Here you invoke it immediately - so why copy at all?
I remember implementing similar code and it was a straight forward recursion - it was called visit
.
namespace details
{
template<size_t puIndex=0, typename PTuple, typename... PTuples, typename PFunc,
std::enable_if_t<puIndex == std::tuple_size_v<std::remove_reference_t<std::remove_const_t<PTuple>>>,int> = 0>
void visit(PFunc&& func, PTuple&& tuple, PTuples&&... tuples)
{}
template<size_t puIndex=0, typename PTuple, typename... PTuples, typename PFunc,
std::enable_if_t<puIndex != std::tuple_size_v<std::remove_reference_t<std::remove_const_t<PTuple>>>,int> = 0>
void visit(PFunc&& func, PTuple&& tuple, PTuples&&... tuples)
{
func(std::get<puIndex>(tuple), std::get<puIndex>(tuples)...);
visit<puIndex+1>(std::forward<PFunc>(func), std::forward<PTuple>(tuple), std::forward<PTuples>(tuples)...);
}
}
template<typename PFunc, typename PTuple, typename... PTuples>
void visit(PFunc&& func, PTuple&& tuple, PTuples&&... tuples)
{
details::visit(std::forward<PFunc>(func), std::forward<PTuple>(tuple), std::forward<PTuples>(tuples)...);
}
I didn't bother making it safe for various sizes nor I needed to deal with return types... which brings me to the second point:
- I don't think this is a good idea to restrict the application to the minimal size of the tuples.
Imagine user wanted to simply work with tuples of the same size and by mistake put a tuple with smaller size. It will cause runtime errors that he will have hard time finding with all the template complexity.
Instead, the default apply_each
should require all tuples to be of the same size and have a special version of apply_each
for the case when the tuples are of varied sizes.
Besides, even in one of cases you've shown some would argue that it should've called the function for the maximal size of the tuples.
About return types... do you really want to deal with that? What if the tuples are in fact arrays and you return a tuple while user wanted an array as output? What to do when some calls return void? It's a mess.
User should just pass an additional tuple to the call as an output parameter and fill it. I don't think overcomplicating the function is worthwhile if the work around is better and quite straightforward.
But if you really need it I'd recommend you write function with a slightly different name to avoid any possible ambiguity. When I think about it - it isn't much more complicated to implement than the visit
above... here is an example:
namespace details
{
template<size_t I, typename Func, typename Tuple, typename... Tuples>
auto call_i(Func&& f, Tuple&& tpl, Tuples&&... tuples)
{
return f(std::get<I>(tpl), std::get<I>(tuples)...);
}
template<typename Func, typename Tuple, typename... Tuples, size_t... I>
auto apply_each_impl(std::index_sequence<I...>, Func&& f, Tuple&& tpl, Tuples&&... tuples)
{
return std::make_tuple(call_i<I>(std::forward<Func>(f), std::forward<Tuple>(tpl), std::forward<Tuples>(tuples)...)...);
}
}
template<typename Func, typename Tuple, typename... Tuples>
auto apply_each(Func&& f, Tuple&& tpl, Tuples&&... tuples)
{
return details::apply_each_impl(std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<std::remove_const_t<Tuple>>>>{}, std::forward<Func>(f), std::forward<Tuple>(tpl), std::forward<Tuples>(tuples)...);
}
But this doesn't work when some of the return types are void
- as one cannot create tuple
with void
template arguments. As @G.Sliepen noticed the call order is not properly defined per C++ standard and GCC calls them in reverse order.
So I believe the only reasonable way to get output without randomized call-order is to literally apply the work-around I proposed earlier but do so automatically. First deduce the output arguments and create the tuple, then apply the visit
with lambda function that calls f
and sets the output values into the output tuple and then return the output tuple. Something like this:
template<typename Func, typename Tuple, typename... Tuples>
auto apply_each_ord(Func&& f, Tuple&& tpl, Tuples&&... tuples)
{
using outputTuple = decltype(apply_each(std::forward<Func>(f), std::forward<Tuple>(tpl), std::forward<Tuples>(tuples)...));
outputTuple out;
visit([&f](auto& outV, auto&&... args)
{
outV = f(args...);
}, out, std::forward<Tuple>(tpl), std::forward<Tuples>(tuples)...);
return out;
}
-
\$\begingroup\$ But how would you implement
tuple_zip
using recursion? \$\endgroup\$G. Sliepen– G. Sliepen2021年09月11日 10:27:33 +00:00Commented Sep 11, 2021 at 10:27 -
1\$\begingroup\$ @G.Sliepen I wouldn't be implementing
tuple_zip
at all - it is just a helper toapply_each
and not part of the main functionality. If you refer to implementation of the case when variables are returned - I wouldn't implement it with recursion. I have added example of an implementation... \$\endgroup\$ALX23z– ALX23z2021年09月11日 11:02:37 +00:00Commented Sep 11, 2021 at 11:02 -
1\$\begingroup\$ Looks good, although it seems to call the function in reverse order on the tuple elements when using GCC (see godbolt.org/z/Wx1Kcn77r). \$\endgroup\$G. Sliepen– G. Sliepen2021年09月11日 12:06:29 +00:00Commented Sep 11, 2021 at 12:06
-
\$\begingroup\$ @G.Sliepen oh... that's not good. I don't see a decent way to deal with this lack of control - I begin to understand why
for...
was not accepted intoC++
standard. Then I believe the only reasonable way to get output without randomized call-order is to literally apply the work-around I proposed earlier but do so automatically. First deduce the output arguments and create the tuple, then apply thevisit
with lambda function that callsf
and sets the output values into the output tuple and then return the output tuple. \$\endgroup\$ALX23z– ALX23z2021年09月11日 12:44:44 +00:00Commented Sep 11, 2021 at 12:44 -
1\$\begingroup\$ @G.Sliepen added
apply_each_ord
implementation that usesapply_each
for type deduction andvisit
for the call. \$\endgroup\$ALX23z– ALX23z2021年09月11日 13:07:51 +00:00Commented Sep 11, 2021 at 13:07
Explore related questions
See similar questions with these tags.