Select & build a compile-time tuple structure based on a runtime-provided mapping

68 Views Asked by At

I am working on a piece that requires some compile-time data structures to be created from a run-time provided mapping. If the runtime provided mapping matches the pre-defined compile-time pattern, the appropriate data structure should be created. However, the compile-time data structure can be defined by any combination of objects or vector of objects. These objects all inherit from the same parent and they are stored in a map using std::variant.

Given that its essentially pattern matching and is allowed to fail if run-time does not provide a pattern that is defined at compile-time, this should be possible somehow...? However, the std::variant seems to be complicating things a lot. Here is what I have so far:

#include <vector>
#include <unordered_map>
#include <string>


template <typename K, typename V>
class DataKV{
public:
    using typeK = K;
    using typeV = V;
};

template <typename... Ts>
class DataKVPackage{
public:
    using DataKVTuple = std::tuple<Ts...>;
    DataKVTuple data_;
    explicit DataKVPackage(DataKVTuple& data): data_(data){}
};

template <typename T>
struct remove_vector {
    using type = T;
};

template <typename T>
struct remove_vector<std::vector<T>> {
    using type = T;
};

template <typename T>
using remove_vector_t = typename remove_vector<T>::type;

template<class Derived, typename... Ts>
class Foo_interface{
public:
    using DataKV_raw_variants = std::variant<remove_vector_t<Ts>...>;
    using DataKV_tuple = std::tuple<Ts...>;
    using DataKVPackage = DataKVPackage<Ts...>;
    Foo_interface()=default;
};

template <typename T>
class Orchestrator{
public:
    std::unordered_map<std::string, typename T::DataKV_raw_variants> map_data;
    void add_data(std::string data_id, const auto& dataKV_ptr){
        map_data[data_id] = typename T::DataKV_raw_variants(dataKV_ptr);
    }
};

// the function I want to implement
template <typename T>
typename T::DataKV_tuple get_tuple(std::unordered_map<int, std::variant<std::string, std::vector<std::string>>> mapping, Orchestrator<T> orch);


// implementation specific to example
using DATA_A = DataKV<int, float>;
using DATA_B = DataKV<double, std::string>;
using DATA_C = DataKV<char, bool>;
class Test : public Foo_interface<Test, std::vector<DATA_A>, DATA_B, std::vector<DATA_C>>{};



// Example usage
int main() {
    Orchestrator<Test> foo;
    DATA_A F_a_0 = DATA_A();
    DATA_A F_a_1 = DATA_A();
    DATA_A F_a_2 = DATA_A();
    DATA_B F_b_0 = DATA_B();
    DATA_B F_b_1 = DATA_B();
    DATA_B F_b_2 = DATA_B();
    DATA_C F_c_0 = DATA_C();
    DATA_C F_c_1 = DATA_C();
    DATA_C F_c_2 = DATA_C();
    foo.add_data("F_a_0", F_a_0);
    foo.add_data("F_a_1", F_a_1);
    foo.add_data("F_a_2", F_a_2);
    foo.add_data("F_b_0", F_b_0);
    foo.add_data("F_b_1", F_b_1);
    foo.add_data("F_b_2", F_b_2);
    foo.add_data("F_c_0", F_c_0);
    foo.add_data("F_c_1", F_c_1);
    foo.add_data("F_c_2", F_c_2);

    // Manual approach:
    std::vector<DATA_A> many_F_a;
    many_F_a.push_back(F_a_0);
    many_F_a.push_back(F_a_1);
    std::vector<DATA_C> many_F_c;
    many_F_c.push_back(F_c_1);
    many_F_c.push_back(F_c_2);
    Test::DataKV_tuple dat_manual = std::make_tuple(many_F_a, F_b_0, many_F_c);
    Test::DataKVPackage data_pack_manual = DataKVPackage<std::vector<DATA_A>, DATA_B, std::vector<DATA_C>>(dat_manual);

    // Can we automate the above? - create a mapping that mirrors above manual approach
    std::unordered_map<int, std::variant<std::string, std::vector<std::string>>> mapping;
    std::vector<std::string> datas_a = {"F_a_0", "F_a_1"};
    std::string data_b = "F_b_0";
    std::vector<std::string> datas_c = {"F_c_1", "F_c_2"};
    mapping[0] = datas_a;
    mapping[1] = data_b;
    mapping[2] = datas_c;
    
    Test::DataKV_tuple dat_auto = get_tuple<Test>(mapping, foo);
//    auto data_pack_auto = Test::DataKVPackage(dat_auto);

    return 0;
}
2

There are 2 best solutions below

2
user3641187 On

So after some research, there does seem to be a way:

#include <vector>
#include <unordered_map>
#include <string>
#include <iostream>


template <typename K, typename V>
class DataKV{
public:
    using value_type = std::nullopt_t;
    using typeK = K;
    using typeV = V;
};

template <typename... Ts>
class DataKVPackage{
public:
    using DataKVTuple = std::tuple<Ts...>;
    DataKVTuple data_;
    explicit DataKVPackage(DataKVTuple& data): data_(data){}
};

template <typename T>
struct remove_vector {
    using type = T;
};

template <typename T>
struct remove_vector<std::vector<T>> {
    using type = T;
};

template <typename T>
using remove_vector_t = typename remove_vector<T>::type;

template<class Derived, typename... Ts>
class Foo_interface{
public:
    using DataKV_raw_variants = std::variant<remove_vector_t<Ts>...>;
    using DataKV_tuple = std::tuple<Ts...>;
    using DataKVPackage = DataKVPackage<Ts...>;
    Foo_interface()=default;
};

template <typename T>
class Orchestrator{
public:
    std::unordered_map<std::string, typename T::DataKV_raw_variants> map_data;
    void add_data(std::string data_id, const auto& dataKV_ptr){
        map_data[data_id] = typename T::DataKV_raw_variants(dataKV_ptr);
    }
};

using DATA_A = DataKV<int, float>;
using DATA_B = DataKV<double, std::string>;
using DATA_C = DataKV<char, bool>;
class Test : public Foo_interface<Test, std::vector<DATA_A>, DATA_B, std::vector<DATA_C>>{};

//template <typename T>
//typename T::DataKV_tuple get_tuple(std::unordered_map<int, std::variant<std::string, std::vector<std::string>>> mapping, Orchestrator<T> orch);


// define main function and example usage

template <typename T>
typename T::DataKV_tuple get_tuple(std::unordered_map<int, std::variant<std::string, std::vector<std::string>>> mapping, Orchestrator<T> &orch) {
    typename T::DataKV_tuple result;

    auto visitor = [&](auto& variant_value, auto& tuple_value) {
        using value_type = std::decay_t<decltype(tuple_value)>; // here can be DataKV<double, std::string> or std::Vector<>DataKV
        if constexpr(std::is_same_v<value_type, std::vector<typename value_type::value_type>>) {
            std::cout<<"vector case"<<std::endl;
            // For vector types
            for (const auto& id : std::get<std::vector<std::string>>(variant_value)) {
                std::cout<<"id: "<<id<<std::endl;
                tuple_value.push_back(std::get<typename value_type::value_type>(orch.map_data.at(id)));
            }
        } else {
            // For non-vector types
            const auto& id = std::get<std::string>(variant_value);
            std::cout<<"non-vector case, id: "<<id<<std::endl;
            tuple_value = std::get<value_type>(orch.map_data.at(id));
        }
    };

    std::apply([&](auto&... args) {
        size_t i = 0;
        ((visitor(mapping[i++], args)), ...);
    }, result);
    return result;
}


// Example usage
int main() {
    Orchestrator<Test> foo;
    DATA_A F_a_0 = DATA_A();
    DATA_A F_a_1 = DATA_A();
    DATA_A F_a_2 = DATA_A();
    DATA_B F_b_0 = DATA_B();
    DATA_B F_b_1 = DATA_B();
    DATA_B F_b_2 = DATA_B();
    DATA_C F_c_0 = DATA_C();
    DATA_C F_c_1 = DATA_C();
    DATA_C F_c_2 = DATA_C();
    foo.add_data("F_a_0", F_a_0);
    foo.add_data("F_a_1", F_a_1);
    foo.add_data("F_a_2", F_a_2);
    foo.add_data("F_b_0", F_b_0);
    foo.add_data("F_b_1", F_b_1);
    foo.add_data("F_b_2", F_b_2);
    foo.add_data("F_c_0", F_c_0);
    foo.add_data("F_c_1", F_c_1);
    foo.add_data("F_c_2", F_c_2);

    // Manual approach:
    std::vector<DATA_A> many_F_a;
    many_F_a.push_back(F_a_0);
    many_F_a.push_back(F_a_1);
    std::vector<DATA_C> many_F_c;
    many_F_c.push_back(F_c_1);
    many_F_c.push_back(F_c_2);
    Test::DataKV_tuple dat_manual = std::make_tuple(many_F_a, F_b_0, many_F_c);
    Test::DataKVPackage data_pack_manual = DataKVPackage<std::vector<DATA_A>, DATA_B, std::vector<DATA_C>>(dat_manual);

    // Can we automate the above? - create a mapping that mirrors above manual approach
    std::unordered_map<int, std::variant<std::string, std::vector<std::string>>> mapping;
    std::vector<std::string> datas_a = {"F_a_0", "F_a_1"};
    std::string data_b = "F_b_0";
    std::vector<std::string> datas_c = {"F_c_1", "F_c_2"};
    mapping[0] = datas_a;
    mapping[1] = data_b;
    mapping[2] = datas_c;
    
    Test::DataKV_tuple dat_auto = get_tuple<Test>(mapping, foo);
    auto data_pack_auto = Test::DataKVPackage(dat_auto);

    std::cout<<"Done"<<std::endl;
    return 0;
}

This does not crash, so it's a start. However, 2 caveats:

DataKV needs an additional typedef "value_type" which then is set to nullptr since it doesn't have a value type. This is because the vector case always (?) needs to be evaluated in the visitor:

if constexpr(std::is_same_v<value_type, std::vector<typename value_type::value_type>>)

Is there perhaps a way to avoid adding this

using value_type = std::nullopt_t;

field...?

Edit: removed another caveat which was just a mistake on my part

0
Aconcagua On

Designed for the RPC mechanism that you describe in comments and assuming that you have unique identifiers of whatever means I recommend having either a std::unordered_map<IdentifierType, std::unique_ptr<Callback>> with Callback being a polymorphic type or alternatively, if identifiers are integral values (transmitted binary or textually) and are dense, an array of such unique_pointers.

The specific callbacks then can be implemented as a variadic template class, thus you'd have to write code only once generically.

This might look to the following:

class Server
{
public:
    // some functions for demonstration...
    void doSomething(std::tuple<int, int> const& parameters);
    void doSomethingElse(std::tuple<std::string> const& parameters);
};

// the virtual base class:
class Callback
{
public:
    virtual ~Callback() { }
    virtual void execute(std::string const& parameters) = 0;
};

// specific implementations as variadic template:
template <typename ... Parameters>
class SpecificCallback : public Callback
{
public:
    SpecificCallback
    (
        Server& server,
        void(Server::*callback)(std::tuple<Parameters...> const&)
    )
        : m_server(server), m_callback(callback)
    { }

    void execute(std::string const& parameters) override
    {
        std::tuple<Parameters...> values;
        parse(parameters, values, std::index_sequence_for<Parameters...>());
        (m_server.*m_callback)(values);
    }

private:
    Server& m_server;
    void(Server::*m_callback)(std::tuple<Parameters...> const&);

    template <size_t ... Indices>
    void parse
    (
        std::string const& parameters,
        std::tuple<Parameters...>& values,
        std::index_sequence<Indices...>
    )
    {
        size_t offset = 0;
        // fold expression, requires C++17:
        ( parse(std::get<Indices>(values), parameters, offset), ... );
    }

    // parsing the individual data types:
    void parse(int& value, std::string const& parameters, size_t& offset);
    void parse(std::string& value, std::string const& parameters, size_t& offset)
};

The individual parsing functions would then parse the parameters string, beginning at offset, into the variable referenced by value, handle values that cannot be parsed appropriately (including string end reached, in which case too few parameters have been provided) and finally advance the offset to the first character following the current parameter.

execute might yet check, before calling the server routines, if offset is equal to parameters.length() to identify too many parameters provided, too.

Usage then might look as follows:

// helper template to create the appropriate unique-pointers,
// hides specifying the template parameters for std::make_unique away
template <typename ... Parameters>
std::unique_ptr<Callback> makeCallback
(
    Server& server,
    void(Server::*callback)(std::tuple<Parameters...> const&)
)
{
    return std::make_unique<SpecificCallback<Parameters...>>(server, callback);
}

Server s;
std::unordered_map<std::string, std::unique_ptr<Callback>> callbacks;
callbacks.emplace("something", makeCallback(s, &Server::doSomething));
callbacks.emplace("somethingElse", makeCallback(s, &Server::doSomethingElse));

// maybe using 0x1f, ASCII unit separator, for separating parameters?
callbacks["something"]->execute("12" "\x1f" "10");
callbacks["somethingElse"]->execute("hello world");

Demonstration on godbolt.