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.