Search code examples
c++inheritancetuplesc++17variadic-templates

elegant way to convert variadic inheritance members to tuple


consider a type that inherits from multiple classes. I want to iterate over the inherited classes, ideally making a get_tuple() member function that returns a reference tuple for precise manipulation:

struct A { int a; };
struct B { float b; };
struct C { const char* c; };

template<typename ... Ts>
struct collection : Ts...{

    constexpr auto get_tuple() const {
        return std::make_tuple(/*???*/);
    }

    std::string string() const {
        std::ostringstream stream;

        auto tuple = this->get_tuple();

        std::apply([&](auto&& first, auto&&... rest) {
            stream << first;
            ((stream << ", " << rest), ...);
            }, tuple);

        return stream.str();
    }
};

int main() {
    collection<A, B, C> a{ 1, 0.5f, "hello" };
    std::cout << a.string();
}

Is there a nice way to achieve this?


this is one solution I found. Giving each base struct a get() function which will enable the std::make_tuple(Ts::get()...); syntax. the tuple will hold the fundamental types (int, float, const char*) but I am not limiting myself to that, I wouldn't also mind a solution where I receive a tuple of A, B and C's. I delete the inherited get(), since these symbols will appear multiple times in collection:

struct A {
    int a; 
    constexpr auto& get() { return a; }
    constexpr const auto& get() const { return a; }
};
struct B {
    float b;
    constexpr auto& get() { return b; }
    constexpr const auto& get() const { return b; }
};
struct C {
    const char* c;
    constexpr auto& get()  { return c; }
    constexpr const auto& get() const { return c; }
};

template<typename ... Ts>
struct collection : Ts...{
    constexpr auto get() = delete;
    constexpr auto get() const = delete;

    constexpr auto get_tuple() {
        return std::make_tuple(Ts::get()...);
    }
    constexpr auto get_tuple() const {
        return std::make_tuple(Ts::get()...);
    }

    std::string string() const {
        std::ostringstream stream;

        auto tuple = this->get_tuple();

        std::apply([&](auto&& first, auto&&... rest) {
            stream << first;
            ((stream << ", " << rest), ...);
            }, tuple);

        return stream.str();
    }
};

int main() {
    collection<A, B, C> a{1, 0.5f, "hello"};
    std::cout << a.string();
}

this works, but is quite a work around, is there an easy solution / some sweet syntax sugar I missed here?


Solution

  • You could rely on the fact that every single one of the base classes allows for structured binding to work with one variable to extract the members.

    
    /**
     * Our own namespace is used to avoid applying AccessMember to arbitrary types
     */
    namespace MyNs
    {
    
    struct A { int a; };
    struct B { float b; };
    struct C { const char* c; };
    
    template<class T>
    constexpr auto& AccessMember(T const& val)
    {
        auto& [member] = val;
        return member;
    }
    
    template<class T>
    constexpr auto& AccessMember(T& val)
    {
        auto& [member] = val;
        return member;
    }
    
    } //namespace MyNs
    
    
    template<typename ... Ts>
    struct collection : Ts...
    {
    
        constexpr auto get_tuple() const
        {
            return std::tuple<decltype(AccessMember(std::declval<Ts>()))...>(AccessMember(static_cast<Ts const&>(*this))...);
        }
    
        constexpr auto get_tuple()
        {
            return std::tuple<decltype(AccessMember(std::declval<Ts&>()))...>(AccessMember(static_cast<Ts&>(*this))...);
        }
    
        std::string string() const {
            std::ostringstream stream;
    
            auto tuple = this->get_tuple();
    
            std::apply([&](auto&& first, auto&&... rest) {
                stream << first;
                ((stream << ", " << rest), ...);
                }, tuple);
    
            return stream.str();
        }
    
    };
    
    int main() {
        using MyNs::A;
        using MyNs::B;
        using MyNs::C;
    
        collection<A, B, C> a{ 1, 0.5f, "hello" };
    
        std::cout << a.string()<< '\n';
    }