Search code examples
c++variadic-templatesparameter-pack

Expand parameter pack into tuple with tuple_cat


Godbolt link: https://godbolt.org/z/18nseEn4G

I have a std::map of various types of vectors (cast to void*) and a T& get<T> method that gives me a reference to an element in one of the vectors in the map.

class Container {
public:
    Container() {
        auto v1 = new std::vector<int>({1, 2, 3, 4, 5});
        auto v2 = new std::vector<char>({'a','b','c','d','e'});
        auto v3 = new std::vector<double>({1.12, 2.34, 3.134, 4.51, 5.101});

        items.insert({
            std::type_index(typeid(std::vector<int>)),
            reinterpret_cast<void*>(v1)
        });
        items.insert({
            std::type_index(typeid(std::vector<char>)),
            reinterpret_cast<void*>(v2)
        });
        items.insert({
            std::type_index(typeid(std::vector<double>)),
            reinterpret_cast<void*>(v3)
        });
    }

    template<typename T>
    T& get(int index) {
        auto idx = std::type_index(typeid(std::vector<T>));
        auto ptr = items.at(idx);
        auto vec = reinterpret_cast<std::vector<T>*>(ptr);
        return (*vec)[index];
    }

private:
    std::map<std::type_index, void*> items {};
};

I want to be able to use structured binding to get back references to 3 elements all at the same index but in difference vectors, but I'm not sure how to create a tuple with multiple calls to the T& get<T> method. Something like this;

auto [a, b, c] = myContainer.get_all<int, char, double>(1); // get a reference to an int, a char, and a double from myContainer at index 1. 

I'm currently trying to make use of repeated calls to T& get<T> for each parameter in a parameter pack, but I can't figure out the correct syntax.

template<typename... Ts>
auto get_all(int index) {
    return std::tuple_cat<Ts...>(
        std::make_tuple<Ts>(get<Ts>(index)...)
    );

How could I make this work? Here is a link to my current attempt: https://godbolt.org/z/18nseEn4G

Alternatively, is there a "better way" to achieve this?


Solution

  • I would suggest using type erasure. Here is an example:

    #include <vector>
    #include <typeindex>
    #include <memory>
    #include <any>
    #include <unordered_map>
    #include <iostream>
    #include <experimental/propagate_const>
    
    // If no library implementation is availble, one may be copied from libstdc++
    template<class T>
    using propagate_const = std::experimental::propagate_const<T>;
    
    class Container
    {
    public:
        Container() {
            std::unique_ptr<Eraser> v1{ static_cast<Eraser*>(new ErasedVector<int>(1, 2, 3, 4, 5)) };
            std::unique_ptr<Eraser> v2{ static_cast<Eraser*>(new ErasedVector<char>('a','b','c','d','e')) };
            std::unique_ptr<Eraser> v3{ static_cast<Eraser*>(new ErasedVector<double>(1.12, 2.34, 3.134, 4.51, 5.101)) };
    
            items[std::type_index(typeid(int))] = std::move(v1);
            items[std::type_index(typeid(char))] = std::move(v2);
            items[std::type_index(typeid(double))] = std::move(v3);
        }
    
        template<typename... Ts>
        std::tuple<Ts&...> get(size_t index)
        {
            return {
                std::any_cast<std::reference_wrapper<Ts>>((*items.find(std::type_index{typeid(Ts)})->second)[index]).get()...
            };
        }
        template<typename... Ts, typename = std::enable_if_t<(std::is_const_v<Ts> && ...)>>
        std::tuple<Ts&...> get(size_t index) const
        {
            return {
                std::any_cast<std::reference_wrapper<Ts>>((*items.find(std::type_index{typeid(Ts)})->second)[index]).get()...
            };
        }
    private:
        class Eraser
        {
        public:
            virtual std::any operator[](size_t index) = 0;
            virtual std::any operator[](size_t index) const = 0;
            virtual ~Eraser() = default;
        };
        template <typename T>
        class ErasedVector : public Eraser
        {
        public:
            template <typename... Args>
            ErasedVector(Args&&... args) :
                data{ std::forward<Args>(args)... }
            {
            }
    
            virtual std::any operator[](size_t index) override final
            {
                return std::reference_wrapper{ data[index] };
            };
            virtual std::any operator[](size_t index) const override final
            {
                return std::reference_wrapper{ data[index] };
            }
        private:
            std::vector<T> data;
        };
    
        std::unordered_map<std::type_index, propagate_const<std::unique_ptr<Eraser>>> items;
    };
    

    It works properly on this example:

    int main()
    {
        Container co;
        auto [i0_0, c0_0, d0_0] = co.get<int, char, double>(0);
        std::cout << i0_0 << ' ' << c0_0 << ' ' << d0_0 << '\n';
        i0_0 = 3; // is a reference
        d0_0 = 42; // is a reference
        auto [i0_1, d0_1] = static_cast<const Container&>(co).get<const int, const double>(0); // works on const Container
        std::cout << i0_1 << ' ' << d0_1; // original values modified
        // i0_1 = 0xDEADBEEF; can be const too
    }
    

    And outputs:

    1 a 1.12
    3 42
    

    Demo