Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mapping const char * to duck-typed T at compile-time or run-time

I have many classes A, B, C, D, etc that are duck-typed and thus have the same methods and interface but do not inherit from the same class.

E.g.

class A {
public:
  void foo();
  void bar();
}
class B {
public:
  void foo();
  void bar();
}
class C {
public:
  void foo();
  void bar();
}

I want to either map const char * to a corresponding instance of one of these classes at run-time e.g.

"A" -> A a

"B" -> B b

Where here a is some instance of class A.

OR map 'const char *` to the the corresponding type at compile-time e.g.

"A" -> A

I need to use the instance of the object in some other functional call (i.e. call foo() or bar()), but the API can only take a const char * as the underlying objects are abstracted away.

I am working in a large, code-genned codebase so changing the paradigm is not practical.

like image 214
biggly Avatar asked Mar 16 '23 17:03

biggly


2 Answers

Perform type-erasure using an adaptor interface and a bunch of concrete adaptors implementing that interface; the adaptors can be instances of a class template.

struct IFooBar {
  virtual ~IFooBar() {}
  virtual void foo() = 0;
  virtual void bar() = 0;
};
template<class T> struct FooBarAdaptor : IFooBar {
  T* t;
  FooBarAdaptor(T* t) : t{t} {} ~FooBarAdaptor() {}
  void foo() override { return t->foo(); }
  void bar() override { return t->bar(); }
};
// ...
  A a;
  B b;
  C c;
  std::map<std::string, std::unique_ptr<IFooBar>> m;
  m["a"] = std::make_unique<FooBarAdaptor<A>>(&a);
  m["b"] = std::make_unique<FooBarAdaptor<B>>(&b);
  m["c"] = std::make_unique<FooBarAdaptor<C>>(&c);
like image 73
ecatmur Avatar answered Mar 18 '23 06:03

ecatmur


Fatal allows you to trivially solve the compile time version of your problem using compile-time strings, type maps and string lookup structures.

Let's first start with the headers we'll be using:

// type_map so we can associated one type to another
#include <fatal/type/map.h>

// for efficient compile-time built string lookup structures
#include <fatal/type/string_lookup.h>

// for compile-time string
#include <fatal/type/sequence.h>

In this example, we basically want to associate strings to actions, both represented by types.

struct foo_action {
  // FATAL_STR creates a compile-time string, equivalent to
  // `using name = fatal::constant_sequence<char, 'f', 'o', 'o'>;`
  FATAL_STR(name, "foo");
  static void DOIT() { std::cout << "FOO-ACTION"; }
};

struct bar_action {
  FATAL_STR(name, "bar");
  static void DOIT() { std::cout << "BAR-ACTION"; }
};

struct baz_action {
  FATAL_STR(name, "baz");
  static void DOIT() { std::cout << "BAZ-ACTION"; }
};

Now we create the mapping from compile-time string to an associated type:

using my_map = fatal::build_type_map<
  foo_action::name, foo_action,
  bar_action::name, bar_action,
  baz_action::name, baz_action
>;

In order to perform an efficient string lookup at runtime, let's create some lookup structure at compile-time since we already have the strings available to the compiler. The actual structure is implementation defined, but it usually uses either a prefix-tree or perfect hashing:

using my_lookup = my_map::keys::apply<fatal::string_lookup>;

Now, we need a visitor that's going to be called whenever there's a match in the lookup.

The visitor will receive the compile-time string as its first parameter, wrapped in a type tag to ensure it's an empty instance.

You can receive any number of additional arguments. In this example, we're receiving a1 and a2 as extra arguments for demonstration purposes. They're not mandatory and could safely be removed:

struct lookup_visitor {
  // note that we don't give the type_tag parameter a name
  // since we're only interested in the types themselves
  template <typename Key>
  void operator ()(fatal::type_tag<Key>, int a1, std::string const &a2) const {
    // Key is the compile-time string that matched
    // now let's lookup the associated type in the map:
    using type = typename my_map::template get<Key>;

    // at this point you have `type`, which is the type associated
    // to `Key` in `my_map`

    // TODO: at this point you can do whatever you like with the mapped type
    // and the extra arguments. Here we simply print stuff and call a method from
    // the mapped type:

    std::cout << "performing action from type '" << typeid(type).name()
      << "' (additional args from the call to exact: a1="
      << a1 << ", a2='" << a2 << "'):";

    // call the static method `DOIT` from the mapped type
    type::DOIT();

    std::cout << std::endl;
  }
};

All that's left to do now is to look the string up in the lookup structure and call the visitor whenever a match is found.

In the code below, we read the runtime string in from the standard input and look it up in the compile-time generated lookup structure.

As stated above, we're also passing two additional arguments to exact(). Those arguments are not inspected by exact(), but rather perfectly forwarded to the visitor. They're completely optional and they're here simply to demonstrate how convenient it is to pass additional state to the visitor.

In this example, the additional arguments are 56 and "test":

int main() {
  for (std::string in; std::cout << "lookup: ", std::cin >> in; ) {
    // exact() calls the visitor and returns true if a match is found
    // when there's no match, the visitor is not called and false is returned
    bool const found = my_lookup::match<>::exact(
      in.begin(), in.end(), lookup_visitor(),
      56, "test"
    );

    if (!found) {
      std::cout << "no match was found for string '" << in << '\''
        << std::endl;
    }
  }

  return 0;
}

Below is a sample output from this code:

$ clang++ -Wall -std=c++11 -I path/to/fatal sample.cpp -o sample && ./sample
lookup: somestring
no match was found for string 'somestring'
lookup: test
no match was found for string 'test'
lookup: foo
performing action from type '10foo_action' (additional args from the call to exact: a1=56, a2='test'): FOO-ACTION
lookup: bar
performing action from type '10bar_action' (additional args from the call to exact: a1=56, a2='test'): BAR-ACTION
lookup: ^D
$

The most interesting part about the code above is that in order to support more mappings, all you need to do is add another entry to my_map. The compiler will figure out the rest.

Update: changed code to reflect latest upstream Fatal.

like image 27
bst Avatar answered Mar 18 '23 05:03

bst