Is there a way to create a compile time type map in C++17 for type-checking?

3.3k views Asked by At

I'm a little new to C++ meta-programming/SFINAE, and I'm having trouble with developing a check to see if a type passed in to a method is contained within a predefined type-list. The context here is that I'd like to check against if the type that's being registered in my variant matches the output type in another structure. Each item that's registered in my application is mapped to another item (in a structure) via a tag (some number). I'd like to create a type-map that can be used at compile time to raise an assertion if the type that is to be registered doesn't match the type of the item in my wire protocol structure.

So something like:

// register elements in type map:
type_map::register(ID_1, decltype(wire_type_item_1)); 
type_map::register(ID_2, decltype(wire_type_item_2));
... etc.

// and when types are registered
template<typename T>
void add_item(const uint32_t id, const T item)
{
   // add static_assert here
   // look up type based on ID, and compare to type passed in
   // when add_item is called
   static_assert(std::is_same<type_map::find(id), decltype(T), 
                 "Attempted to register type that does not match wire type"); 

   ...
} 

I'd appreciate any pointers on where to start/how to go about doing this - thanks!

4

There are 4 answers

0
Ayjay On

Here's one approach for creating a compile-time map between classes.

#include <type_traits>

struct foo {};
struct bar {};
struct ID_1 {};
struct ID_2 {};
struct ID_3 {};

template <class T>
struct type_map {};

template <>
struct type_map<ID_1> { using type = foo; }; // map ID_1 -> foo
template <>
struct type_map<ID_2> { using type = bar; }; // map ID_2 -> bar

int main() {
    static_assert(std::is_same_v<type_map<ID_1>::type, foo>);
    static_assert(std::is_same_v<type_map<ID_2>::type, bar>);
    static_assert(std::is_same_v<type_map<ID_3>::type, int>); // fails, ID_3 isn't in map
    static_assert(std::is_same_v<type_map<ID_1>::type, bar>); // fails, ID_1 is mapped to foo, not bar
}

In your code sample, though, you have the following line: static_assert(std::is_same<type_map::find(id), decltype(T), "Attempted to register type that does not match wire type"); The issue with this is that id is a runtime variable. You can't use static_assert at run-time.

If you do want to lookup types in this map at runtime, it's a bit more complicated. I would suggest using a metaprogramming library like hana, as it allows you to do things like loop through all elements of a compile-time data structure at run time.

3
max66 On

Not sure about what do you exactly want... anyway...

Surely you can't use a const std::uint32_t id value to obtain (find(id)) a type (as a value of a function?) that you can use in a static_assert()

template <typename T>
void add_item (std::uint32_t const id, T const item)
// ................................^^  the value id is unusable in a static_assert()

If you know the value of id compile time (otherwise your question do not make sense) you can pass it (I suggest as std::size_t) as template value in a std::integral_constant

template <std::size_t ID, uintypename T>
void add_item (std::integral_constant<std::size_t, ID>, T const item)

or better, I suppose, directly as template parameter that you have to explicit calling add_item().

Anyway... for the type map I propose, first of all, a ct_pair (compile time pair) between a std::size_t and a type

template <std::size_t, typename>
struct ct_pair
 { };

Given also a couple of helper structs as follows

template <std::size_t, std::size_t, typename>
struct get_tuple
 { using type = std::tuple<>; };

template <std::size_t I, typename T>
struct get_tuple<I, I, T>
 { using type = std::tuple<T>; };

you can create a ct_map (compile time map), using template specialization, the power of std::tuple_cat() together with std::get_val() and decltype() as follows

template <typename ...>
struct ct_map;

template <std::size_t ... Is, typename ... Ts>
struct ct_map<ct_pair<Is, Ts>...>
 {
   template <std::size_t I>
   static constexpr auto find_type_func ()
    -> decltype( std::get<0>( std::tuple_cat(
             std::declval<typename get_tuple<I, Is, Ts>::type>()...)) );

   template <std::size_t I>
   using find_type
      = std::remove_reference_t<decltype( find_type_func<I>() )>;
 };

To register the elements on a map you have to define a using

using type_map = ct_map<ct_pair<2u, char>,
                        ct_pair<3u, int>,
                        ct_pair<5u, long>,
                        ct_pair<7u, long long>>;

and the static_assert() check become something as follows

static_assert( std::is_same_v<type_map::find_type<5u>, long> );

The following is a full compiling C++17 example

#include <tuple>
#include <iostream>
#include <type_traits>

template <std::size_t, typename>
struct ct_pair
 { };

template <std::size_t, std::size_t, typename>
struct get_tuple
 { using type = std::tuple<>; };

template <std::size_t I, typename T>
struct get_tuple<I, I, T>
 { using type = std::tuple<T>; };

template <typename ...>
struct ct_map;

template <std::size_t ... Is, typename ... Ts>
struct ct_map<ct_pair<Is, Ts>...>
 {
   template <std::size_t I>
   static constexpr auto find_type_func ()
    -> decltype( std::get<0>( std::tuple_cat(
             std::declval<typename get_tuple<I, Is, Ts>::type>()...)) );

   template <std::size_t I>
   using find_type
      = std::remove_reference_t<decltype( find_type_func<I>() )>;
 };

using type_map = ct_map<ct_pair<2u, char>,
                        ct_pair<3u, int>,
                        ct_pair<5u, long>,
                        ct_pair<7u, long long>>;

int main()
 {
   static_assert( std::is_same_v<type_map::find_type<5u>, long> );
 }
0
Yakk - Adam Nevraumont On
template<auto i>
using constant = std::integral_constant<decltype(i), i>;

template<class T>
struct tag_t {
  using type=T;
  // comparison with other tags!
  constexpr auto operator==( tag_t<T> ) const { return std::true_type{}; }
  constexpr auto operator!=( tag_t<T> ) const { return std::false_type{}; }
  template<class U>
  constexpr auto operator==( tag_t<U> ) const { return std::false_type{}; }
  template<class U>
  constexpr auto operator!=( tag_t<U> ) const { return std::true_type{}; }
};
template<class T>
constexpr tag_t<T> tag = {};

Now we can work with integers and types as values.

type_map::register(ID_1, decltype(wire_type_item_1)); 
type_map::register(ID_2, decltype(wire_type_item_2));
... etc.

this becomes

auto wire_type_map( constant<ID_1> ) { return tag<wire_type_item_1>; }
auto wire_type_map( constant<ID_2> ) { return tag<wire_type_item_1>; }

this registration can be done in a distributed manner, so long as it is visible (from header files) at point of use.

// and when types are registered
template<typename T, uint32_t id>
void add_item(constant<id> id_tag, const T item)
{
   static_assert(wire_type_map( id_tag ) == tag<T>, 
             "Attempted to register type that does not match wire type"); 

   ...
} 

this does require that the added item's integer value be a compile time constant at this point.

Often if you are reading the integer and the data off the wire, this isn't practical; there is no way to check at compile time that the integer you are reading matches a type. But there is the ability to check at compile time that the integer you are providing matches the type.

OTOH, it might be easier to strip the integer argument entirely, and map the type to the integer, instead.

0
max66 On

Following (and misunderstanding?) a suggestion in a comment from Yakk (thanks) at my first answer, I've written a really different (simpler and more elegant, IMHO) way to implement the compile time map.

First of all, the ct_pair. Now is more complex with a using type type (to get the template type T parameter) and a static function that, given the same index of the struct (wrapped in a std::integral_constant, to have different types from different implementation of ct_pair)

template <std::size_t I, typename T>
struct ct_pair
 { 
   using type = T;

   static ct_pair get_pair (std::integral_constant<std::size_t, I>)
    { return {}; }
 };

Now std::tuple, std::tuple_cat() and the helper class get_tuple aren't needed anymore and ct_map simply become

template <typename ... Ps>
struct ct_map : public Ps...
 {
   using Ps::get_pair...;

   template <std::size_t I>
   using find_type
      = typename decltype(get_pair(
            std::integral_constant<std::size_t, I>{}))::type;
 };

Now the full example (unfortunately C++17 and not before, when the other my answer should be C++11/C++14 compatible) become

#include <tuple>
#include <iostream>
#include <type_traits>

template <std::size_t I, typename T>
struct ct_pair
 { 
   using type = T;

   static ct_pair get_pair (std::integral_constant<std::size_t, I>)
    { return {}; }
 };

template <typename ... Ps>
struct ct_map : public Ps...
 {
   using Ps::get_pair...;

   template <std::size_t I>
   using find_type
      = typename decltype(get_pair(
            std::integral_constant<std::size_t, I>{}))::type;
 };

using type_map = ct_map<ct_pair<2u, char>,
                        ct_pair<3u, int>,
                        ct_pair<5u, long>,
                        ct_pair<7u, long long>>;

int main()
 {
   static_assert( std::is_same_v<type_map::find_type<5u>, long> );
 }