Search code examples
c++tuplesvariadic-templates

const tuple to tuple of consts?


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?


Solution

  • 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.