1

My ultimate goal is to achieve a mapping from a set of two character strings to corresponding data types:

"AA" --> char
"BB" --> int
"CC" --> float
"DD" --> std::complex<double>
and so on ...

The best "not quite working" solution I can come up with is two parts. The first part is using std::map to map between the strings and corresponding enum values. The second part uses a templated type alias and std::conditional to map the enum values to types.

enum class StrEnum { AA, BB, CC, DD };
// STEP 1: string --> enum
// !! std::map will not work here since a constexpr is required !!
std::map<std::string, StrEnum> str_map = {
    {"AA", StrEnum::AA},
    {"BB", StrEnum::BB},
    {"CC", StrEnum::CC},
    {"DD", StrEnum::DD}
};
// STEP 2: enum --> type
template<StrEnum val> using StrType = typename std::conditional<
   val == StrEnum::AA,
   char,
   typename std::conditional<
       val == StrEnum::BB,
       int,
       typename std::conditional<
           val == StrEnum::CC,
           float,
           std::complex<double>
       >::type
   >::type
>::type;

Goal Usage: StrType<str_map["BB"]> myVal; // <-- does not work in current state with std::map

As more value mappings are added the above nesting can get very nasty.

Is there a better/cleaner/working overall way to achieve this mapping? I'm especially interested in STEP 2 and whether there is a way to have less nesting.

I am using C++11. (but if the only answer lies in C++14 or beyond it would be nice to at least be aware of it)

majorpain1588
  • 319
  • 1
  • 8
  • To be clear: You want this mapping to work at compile-time? – Max Langhof Mar 15 '19 at 13:09
  • 4
    Does this code actually work as is? – NathanOliver Mar 15 '19 at 13:10
  • 2
    [Hana](https://www.boost.org/doc/libs/1_65_0/libs/hana/doc/html/structboost_1_1hana_1_1map.html) might work for you, but might be a little awkward turning the two-character keys into types. – chris Mar 15 '19 at 13:11
  • Note that your code is missing several `typename` (before each `std::conditional`) to be compliant. I guess you are using MSVC? – Max Langhof Mar 15 '19 at 13:13
  • My bad on the missing typenames. I edited the post to add them in. I'm actually using gcc as my compiler. @ Max Langhof, this mapping should work at compile time ideally. – majorpain1588 Mar 15 '19 at 13:31
  • Looking more closely it looks like the std::map part probably doesn't work as is given the use case of using it in the template argument. But the code does work starting with an Enum or Int value and foregoing the strings. Maybe starting with string is just a step too far. – majorpain1588 Mar 15 '19 at 13:40
  • to map strings to types, this might help: https://stackoverflow.com/a/1826505/4117728 – 463035818_is_not_an_ai Mar 15 '19 at 13:46
  • Actually, your solution does not work at all. If you want to get a *type* everything should be computable at *compile-time*. So, at this point, what is the purpose of having a string (you cannot use at runtime)? So, just "*map*" the type with `enum`. – BiagioF Mar 15 '19 at 14:06

2 Answers2

3

Since std::map does not have constexpr ctors, your template argument str_map["BB"] can not be evaluated at compile-time.

A simple and maintainable way to map integers to types would be using std::tuple and std::tuple_element as follows. For instance, StrType<0> is char, StrType<1> is int, and so on:

using types = std::tuple<char, int, float, std::complex<double>>;

template<std::size_t N>
using StrType = typename std::tuple_element<N, types>::type;

Then the problem is how to map strings to integers in C++11. First, strings can be compared at compile-time by the accepted answer in this post. Second, we can use the ternary operator in compile-time evaluations. Thus, at least the following function getIdx can map each string to the corresponding integer at compile-time. For instance, getIdx("AA") is zero:

constexpr bool strings_equal(const char* a, const char* b) {
    return *a == *b && (*a == '\0' || strings_equal(a + 1, b + 1));
}

constexpr std::size_t getIdx(const char* name) 
{
    return strings_equal(name, "AA") ? 0:
           strings_equal(name, "BB") ? 1:
           strings_equal(name, "CC") ? 2:
           strings_equal(name, "DD") ? 3:
                                       4; // compilation error
}

You can use these functions for the current purpose as follows:

DEMO

StrType<getIdx("BB")> x; // x is int.

constexpr const char* float_type = "CC";
StrType<getIdx(float_type)> y; // y is float.

static_assert(std::is_same<StrType<getIdx("AA")>, char> ::value, "oops."); // OK.
static_assert(std::is_same<StrType<getIdx("BB")>, int>  ::value, "oops."); // OK.
static_assert(std::is_same<StrType<getIdx("CC")>, float>::value, "oops."); // OK.
static_assert(std::is_same<StrType<getIdx("DD")>, std::complex<double>>::value, "oops."); // OK.
Hiroki
  • 2,780
  • 3
  • 12
  • 26
  • 2
    I think this basically solves it. The constexpr string comparison function was the real missing link I would have never thought of. – majorpain1588 Mar 15 '19 at 19:26
1

I recentlly worked on something like that. My proposed solution was something like this (with a lot more stuff but here the main idea):

//Define a Type for ID
using TypeIdentifier = size_t;

//Define a ID generator
struct id_generator {
    static TypeIdentifier create_id() {
        static TypeIdentifier value = 0;
        return value++;
    }
};

//Define some kind of handler for place 
struct type_id_handler {
    static std::vector<std::function<void(void*)>> type_handler;
};

std::vector<std::function<void(void*)>> type_id_handler::type_handler;

//Define id's and make a basic functions
template<typename T>
struct type_id_define {
    static TypeIdentifier get_id() {
        static TypeIdentifier id = id_generator::create_id();
        static auto one_time_stuff = [] () -> bool {
            type_id_handler::type_handler.resize(id+1);
            type_id_handler::type_handler[id] = [](void* ptr) {
                auto * object = static_cast<T*>(ptr);
                //do stuff your type
                std::cout << __PRETTY_FUNCTION__ << std::endl;
            };
            return true;
        }();

        return id;
    }
};

For main dummy test:

int main() {

    std::map<std::string, TypeIdentifier> typeMap {
            {"AA", type_id_define<char>::get_id()},
            {"BB", type_id_define<int>::get_id()},
    };

    int a;
    char b;
    type_id_handler::type_handler[typeMap["BB"]](&a);
    type_id_handler::type_handler[typeMap["AA"]](&b);
    return 0;
}

Output should be:

type_id_define<T>::get_id()::<lambda()>::<lambda(void*)> [with T = int]
type_id_define<T>::get_id()::<lambda()>::<lambda(void*)> [with T = char]

The main idea is to make a new type_id_define with the proper id for each type and use it as a index for select the correct function to execute. Also when generating the id it store a function for cast from void* to given type (I use void* for store different type function on same vector) After that you can use std::any, void*, or whatever you want for pass the object to the function and get type-safety.

If you want to use something like that I Also recommend you to think about better way for register types and add the corresponding function.

Enoc Martinez
  • 165
  • 1
  • 4