2

I have no hope that what I'd like to achieve is possible in C++, but maybe I'm wrong since my previous question about bidirectional static mapping got an unlikely answer.

I have a set of certain types, an enumeration with keys representing the types, and a template handle type that accepts mentioned types as template parameters.

struct foo {};
struct bar {};

enum class types { foo, bar };

template<typename T>
struct qux {};

I'd like to be able to map types::foo to foo at runtime. Most of the time foo would be used as the template parameter of qux, so mapping of types::foo to qux<foo> is fine too but I feel that if one is possible, then the other is too.

I'm mentioning this because it's important to note that qux is a simple handle-like type that only consists of an index and is passed around by value and there are a lot of template functions that take qux<T> as a parameter.

This makes polymorphism - a standard solution in such cases - not an obvious choice.

Sometimes though I need to create a qux<T> while having only a variable holding a types value, so it has to be mapped to the proper type somehow.

What I've been doing up until now is just switching each time I have to do this but I hit the point where there's too many switches to maintain.

I don't see a better solution, so what I'm looking to do is create a single swich or other mechanism in the code that will take types value and return... something that will let me create a qux<T> with related type.

Ultimately it'd work like this.

template<typename T>
void baz(qux<T> q) { /* ... */ }

// Somewhere else...
types t = get_type(); // Read type at runtime.
baz(create_object(t)); // Calls adequate baz specialization based on what value t contains.

I don't know how to implement the create_object function though.


What I tried already:

  1. std::variant with careful use of emplace and index - quickly hit the problem of being unable to return different types from a single function;
  2. clever use of conversion operators - doesn't allow me to call a proper templated function taking qux<T> as a parameter since it's not decided which specialization should be called;
  3. external polymorphism - unable to return different types;
  4. modified template specialization loop proposed in this answer that looked for proper types value and returned mapped type - this failed due to being unable to return different types - or called a lambda with auto parameter - which also failed as it tried to specialize the lambda multiple times.
Vennor
  • 596
  • 6
  • 13
  • Templates are instantiated at compile-time. `qux::` needs to be instantiated at compile-time, which won't happen if the mapping happens at run-time. – AndyG Jul 06 '20 at 15:48
  • The best you can really do here if you go down that route is to perform some kind of type erasure such that you have a type-erased class (like `void*`) that gets returned from some `create` function that receives some input, like a hash_code from a `type_info` struct and has the correct mapping to a function that returns a `qux` type-erased as `void*`, and then the caller has to perform a `static_cast` to recover the type. – AndyG Jul 06 '20 at 15:53
  • But it sounds like you want an auto-registering factory pattern where the types *are* known at compile-time. – AndyG Jul 06 '20 at 15:54
  • 1
    `std::variant` seems like exactly the right tool here. What did you try? In your example, `create_object` should return `std::variant, qux>` and you'd call `baz` with `std::visit([&](auto &&x) -> decltype(auto) { return baz(std::forward(x)); }, create_object(t))` (yes, the incantation is long, but there's a lot going on to make this work). Heck, `std::variant` works so well it can straight-up replace `types`: `using types = std::variant, std::type_identity>`, which might help clean things up (or might not, idk). – HTNW Jul 06 '20 at 17:17
  • 1
    Does this answer your question? [Too many if/else statements in converting user inputs to types in C++](https://stackoverflow.com/questions/61309900/too-many-if-else-statements-in-converting-user-inputs-to-types-in-c) – HTNW Jul 06 '20 at 18:17
  • @HTNW I believe it does but judging by how convoluted the solution is I'd rather use `std::variant` with `std::visit` as you suggested. I tested your approach and although it requires lambda expressions for each usage it works fine and is even more powerful than I expected it to be. For now I implemented an imitation of `std::visit` inside the type I use to pass around the generalised handle. If you make an answer out of your comment regarding `std::variant` I'll gladly accept it. – Vennor Jul 07 '20 at 14:44
  • 1
    @Vennor I've gone and simplified my answer there. I have no idea why I made it so complicated. If you've implemented `std::visit` inside `types` then you've basically made `types` into `std::variant` anyway (`std::variant` basically *is* `std::visit`). My suggestion to have `create_object` return a `std::variant` is but a variation on that idea. – HTNW Jul 07 '20 at 16:54

1 Answers1

1

std::visit is your friend here. Convert types to a certain std::variant/replace it with an alias to that type:

// or std::type_identity
template<typename T> struct proxy { using type = T };
template<typename T> constexpr inline proxy<T> proxy_v{};
using var_types = std::variant<proxy<foo>, proxy<bar>>;
var_types mk_var_types(types t) {
    switch(t) {
        case types::foo: return proxy_v<foo>;
        case types::bar: return proxy_v<bar>;
    }
}

/write a custom std::visit-like for types (all three choices are equivalent, but replacing types is the shortest)

template<typename F>
decltype(auto) visit(F &&f, types t) {
   switch(t) {
       case types::foo: return std::forward<F>(f)(proxy_v<foo>);
       case types::bar: return std::forward<F>(f)(proxy_v<bar>);
   }
}

This can be used to implement a std::variant-of-quxs-returning create_object

auto create_object(var_types t) {
    std::visit([](auto p) -> std::variant<qux<foo>, qux<bar>> { return qux<typename decltype(p)::type>{} };, t);
}
// or
auto create_object(types t) {
    return create_object(mk_var_types(t));
}
// or
auto create_object(types t) {
     return visit([](auto p) -> std::variant<qux<foo>, qux<bar>> { return qux<typename decltype(p)::type>{}; }, t);
}

Which can be used to call baz

types t;
// or
var_types t;
std::visit([](auto &&q) { baz(std::forward<decltype(q)>(q)); }, create_object(t));

Of course, create_object isn't necessary in this case

visit([](auto p) { baz(qux<typename decltype(p)::type>{}); }, /*mk_var_types(*/t/*)*/);

Repeating foo and bar everywhere is itself a pain. This can be rectified:

template<template<typename> typename F>
using variant_with = std::variant<F<foo>, F<bar>>;
using var_types = variant_with<proxy>;
using a_qux = variant_with<qux>;
a_qux create_object(a_type); // etc.
HTNW
  • 27,182
  • 1
  • 32
  • 60