-2

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;
}
user3641187
  • 405
  • 5
  • 10
  • You cannot move from runtime to compile time (at least not in C++). Compile time ALWAYS happens first (and then is baked into your code). Or maybe the title of your question is wrong and you want to select any of the precompiled structures based on runtime information? – Pepijn Kramer May 30 '23 at 12:52
  • Doesn't seem possible. – Jason May 30 '23 at 12:53
  • What's the reason for a dynamically built structure not to be considered? Many tools (e.g. XML DOM parsers) work that way and are pretty successful with. – Aconcagua May 30 '23 at 12:54
  • @PepijnKramer If I read the question right it is about providing at compile-time a number of different pre-built types matching a sub-set of all thinkable setups and at run-time reading in some kind of structure description and matching it against the list of these pre-builts; if any one found, fine, otherwise error... – Aconcagua May 30 '23 at 12:59
  • @Aconcagua Yes exactly. So the code is allowed to fail if the mapping does not strictly follow the compile-time preset. Hence, I do think this should be possible since its pattern-matching – user3641187 May 30 '23 at 13:04
  • @Pepijn Kramer You are right with "select any of the precompiled structures based on runtime information" I'll try to change the title – user3641187 May 30 '23 at 13:05
  • What do you actually want to achieve that way? Sounds like an [XY-problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) to me... – Aconcagua May 30 '23 at 13:05
  • It's in the context of a RPC call. The RPC server has pre-defined compile time structures that can be built out of individual components (they must just adhere to the compile-time structure). the client sends a string which gets parsed to the mapping and then the object gets created – user3641187 May 30 '23 at 13:07
  • Aren't you going to select the specific function to call on the server by some kind of *identifier*? Unique function name, maybe even a numerical id? This would cover the case of multiple different functions having the same parameters, and from the identifier on it would be clear which parameters are needed/how they need to be parsed. If you now pack them into a struct or pass them as individual parameters to the function actually doesn't matter any more... – Aconcagua May 30 '23 at 13:22
  • In case of such identifiers existing I'd have a `std::map>` with `ParameterParser` being a polymorphic class – or maybe just an array of in case of dense numeric identifiers (no matter if binary or textual representation). On receiving a RPC message you'd split the identifier and the parameters, select the right parameter parser (and handle the case it is not found) and the specific implementations then create your structs and pass them to the actual function to execute the RPC. – Aconcagua May 30 '23 at 13:33
  • @Aconcagua your approach would be work yes. However, in this case the main issue results from the fact that I want to avoid hard-coding the different parser types. With above approach, I essentially construct the parsers at compile time, directly inferred from other parts of the code. It actually does work to "some" degree, I've added an attempt now. – user3641187 May 30 '23 at 14:30
  • You wouldn't necessarily hard-code the all parser types – well, at least not *individually* – you might have a variadic template class for... – Aconcagua May 30 '23 at 14:34

2 Answers2

0

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

user3641187
  • 405
  • 5
  • 10
  • you can detect if it's a vector, by overload the visitor or traits in if constexpr. – apple apple May 30 '23 at 17:32
  • but I don't really see why it's a problem at first place. the deserializer should already return the correct type, or variant of correct type. – apple apple May 30 '23 at 17:40
0

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.

Aconcagua
  • 24,880
  • 4
  • 34
  • 59