const tuple to tuple of consts?

Viewed 282

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?

2 Answers

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:

1. Deduce the type of 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);
}

Live Demo

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);
}

Live Demo

2. Create a new tuple containing references to the original tuple's contents

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);
}

Live Demo

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.

I think this is an okay use of const_cast to pretend that your mutable parameter is const. It may be UB, I'm no expert.

Basically, put the logic in the const version for const-correctness. Then the non-const version calls the const version after casting away the const from the reference.

First, removing a const from a reference (might be a better way to do this):

// badly named but turns `const int &` -> `int &`
template <typename T>
struct remove_const_ref {
  using type = std::add_lvalue_reference_t<
      std::remove_const_t<std::remove_reference_t<T>>>;
};
template <typename T>
using remove_const_ref_t = typename remove_const_ref<T>::type;

Then, the const version:

template <size_t I = 0, typename F, typename... Tp>
static void apply_to_foos(const 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);
}

And the non-const version, which just casts and calls:

template <size_t I = 0, typename F, typename... Tp>
static void apply_to_foos(std::tuple<Tp...> &t, F func) {
  apply_to_foos(std::as_const(t), [&func](const auto &v) {
    func(const_cast<remove_const_ref_t<decltype(v)>>(v));
  });
}

Example:

int main() {
  std::tuple thingMut = {1, 2, 3, 4};
  const std::tuple thingConst = {1, 2, 3, 4};

  apply_to_foos(thingMut, [](auto &v) {
    v++;
    std::cout << "mut frob: " << v << std::endl;
  });
  apply_to_foos(thingConst, [](auto &v) {
    // v++;  error: you cannot assign to a variable that is const
    std::cout << "const frob: " << v << std::endl;
  });
}
Related