If I have a tuple member variable where the types in the tuple are a parameter pack of a class template, I can apply a function to each object in the tuple with a static member function like this:
template<size_t I = 0, typename F, typename... Tp>
static void apply_to_foos(std::tuple<Tp...>& t, F func) {
auto& foo = std::get<I>(t);
func(foo);
if constexpr (I + 1 != sizeof...(Tp))
apply_to_foos<I + 1>(t, func);
}
so for example, if foos_
is the tuple member variable, I could implement a member function over the foos via:
void frobnicate() {
apply_to_foos( foos_, [](auto& f){f.frob();} );
}
etc. however, if frobnicate
had been const I run into the problem that foos_
will now be const, and apply_to_foos
wants a non-const tuple. So for example this won't work (if foos are some kind of containers)
size_t size() const {
size_t sz = 0;
apply_to_foos( foos_, [&sz](auto& f){ sz += f.size();} );
return sz;
}
I could implement an overload of apply_to_foos
that takes a const std::tuple<Tp...>&
, const_cast to a non-const tuple and call the original overload, but, well, then I am just casting away const-ness.
The annoying thing about it is that the only part of apply_to_foos
that cares about const-ness is the signature. Everything else will work with const values as long the const-ness is respected e.g. the lambda will need to take a reference to const value, etc. If I could cast from a const tuple to a tuple of consts then I could implement the const overload like this:
template<size_t I = 0, typename F, typename... Tp>
static void apply_to_foos(const std::tuple<Tp...>& t, F func) {
auto& tup_of_consts = const_tuple_to_tuple_of_consts(t);
apply_to_foos(tup_of_consts, func);
}
and the size()
member function would just work naturally ... I feel like there must be an easier way to handle this though?
You can't cast a const std::tuple<T1, T2>
to std::tuple<const T1, const T2>
without making copies of the contained items.
You have two options for what you can do though:
t
and sidestep the problem entirely:template<size_t I = 0, typename F, typename T>
static void apply_to_foos(T& t, F func) {
auto& foo = std::get<I>(t);
func(foo);
if constexpr (I + 1 != std::tuple_size<T>::value)
apply_to_foos<I + 1>(t, func);
}
With the type of t
deduced, both situations will work. If passed a const
tuple then T
will be decuded to const std::tuple<...>
making the type of t
const std::tuple<...>&
and if passed a non-const
tuple then T
will be deduced to std::tuple<...>
and the type of t
will be std::tuple<...>&
. The only other change required is to use std::tuple_size<T>::value
in place of sizeof...(Tp)
. Note that this also allows apply_to_foos
to work with other types like std::array
and std::pair
.
You'll probably also want to apply perfect-forwarding to allow apply_to_foos
to work with rvalues and preserve the value category of the tuple elements as well. For example:
template<size_t I = 0, typename F, typename T>
static void apply_to_foos(T&& t, F func) {
func(std::get<I>(std::forward<T>(t)));
if constexpr (I + 1 != std::tuple_size<std::remove_reference_t<T>>::value)
apply_to_foos<I + 1>(std::forward<T>(t), func);
}
Create a second overload of apply_to_foos
that accepts a reference to a const
tuple and creates a temporary tuple of const
references to the original tuple's elements:
template<typename F, typename... Tp>
static void apply_to_foos(const std::tuple<Tp...>& t, F func) {
std::tuple<std::add_const_t<Tp>&...> tuple_of_const = t;
apply_to_foos(tuple_of_const, func);
}
This will work fine, but it has issues with value category preservation. The elements of a tuple rvalue will be passed to the callback function as const
lvalues and can't easily be moved-from for example.