C++ Compile-Time Type Map

Introduction

In C++ template programming, sometimes I would like to declare a variable of certain type based on a query type. This requires creating a compile-time type map that maps the key type to the value type.

In this article, I will show how to implement a compile-time type map in C++ using C++ 17 features.

C++ Specialized Compile-Time Type Map

It’s possible to create specialized compile-time type maps using std::conditional_t and std::is_same_v or template specialization. But it is often inconvenient if there are many different type mappings in the map or there are many such maps to be defined.

In the following example, we used two different ways to define a specialized compile-time type map that maps the key type to the value type. More specifically, the type maps map int16_t to uint16_t, int32_t to uint32_t, and int64_t to uint64_t.

specialized_compile_time_type_map.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include <cstdint>
#include <type_traits>

template <typename KeyType, typename... KeyTypes>
constexpr bool has_duplicated_keys()
{
// C++ 17 fold expression.
return (
std::is_same<typename KeyType::key, typename KeyTypes::key>::value ||
...);
}

template <typename QueryType, typename... KeyTypes>
constexpr bool has_key()
{
return has_duplicated_keys<QueryType, KeyTypes...>();
}

template <typename KeyType, typename ValueType>
struct TypePair
{
using key = KeyType;
using value = ValueType;
};

template <typename QueryType>
struct SpecializedTypeMap0
{
// Perform the following specialized type mapping.
// K0 -> V0
// K1 -> V1
// K2 -> V2

using K0 = int16_t;
using K1 = int32_t;
using K2 = int64_t;

using V0 = uint16_t;
using V1 = uint32_t;
using V2 = uint64_t;

// Perform the type mapping.
using type_map_0 = TypePair<K0, V0>;
using type_map_1 = TypePair<K1, V1>;
using type_map_2 = TypePair<K2, V2>;

static_assert(!has_duplicated_keys<type_map_0, type_map_1, type_map_2>(),
"Duplicate key values.");
static_assert(has_key<TypePair<QueryType, QueryType>, type_map_0,
type_map_1, type_map_2>(),
"Key not found.");

// Find type using std::conditional_t.
using find_type = std::conditional_t<
std::is_same_v<QueryType, K0>, V0,
std::conditional_t<std::is_same_v<QueryType, K1>, V1, V2>>;
};

template <typename QueryType>
struct SpecializedTypeMap1
{
};

// Create a type map by specializing the type map.
template <>
struct SpecializedTypeMap1<int16_t>
{
using find_type = uint16_t;
};

template <>
struct SpecializedTypeMap1<int32_t>
{
using find_type = uint32_t;
};

template <>
struct SpecializedTypeMap1<int64_t>
{
using find_type = uint64_t;
};

int main()
{
static_assert(
std::is_same_v<SpecializedTypeMap0<int16_t>::find_type, uint16_t>);
static_assert(
std::is_same_v<SpecializedTypeMap0<int32_t>::find_type, uint32_t>);
static_assert(
std::is_same_v<SpecializedTypeMap0<int64_t>::find_type, uint64_t>);
// Unable to compile because the key does not exist in the type map.
// static_assert(
// std::is_same_v<SpecializedTypeMap0<int8_t>::find_type, uint8_t>);
static_assert(
std::is_same_v<SpecializedTypeMap1<int16_t>::find_type, uint16_t>);
static_assert(
std::is_same_v<SpecializedTypeMap1<int32_t>::find_type, uint32_t>);
static_assert(
std::is_same_v<SpecializedTypeMap1<int64_t>::find_type, uint64_t>);
// Unable to compile because the key does not exist in the type map.
// static_assert(
// std::is_same_v<SpecializedTypeMap1<int8_t>::find_type, uint8_t>);
}

To build and run the program, please run the following commands.

1
2
$ g++ specialized_compile_time_type_map.cpp -o specialized_compile_time_type_map --std c++17
$ ./specialized_compile_time_type_map

We could see that if there are many different type mappings in the map, find_type will become very long and hard to maintain. In addition, the code in the SpecializedTypeMap0 and SpecializedTypeMap1 classes is not quite reusable.

C++ Generic Compile-Time Type Map

To create a generic compile-time type map, we created the following implementation. The generic compile-time type map can be used for creating multiple maps conveniently, similar to a std::map value map that is used at runtime.

generic_compile_time_type_map.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <cstdint>
#include <tuple>
#include <type_traits>

template <typename TypePair, typename... TypePairs>
constexpr bool has_duplicated_keys()
{
// C++ 17 fold expression.
return (
std::is_same<typename TypePair::key, typename TypePairs::key>::value ||
...);
}

template <typename KeyType, typename ValueType>
struct TypePair
{
using key = KeyType;
using value = ValueType;

// Is does not matter what exactly the type is, as long as the type contains
// the key type. The following also works.
// static TypePair get_pair_instance(std::tuple<KeyType, KeyType>)
static TypePair get_pair_instance(std::is_same<KeyType, KeyType>)
{
// This can always be constructed.
return TypePair{};
}
};

template <typename... TypePairs>
struct TypeMap : public TypePairs...
{
using TypePairs::get_pair_instance...;

static_assert(!has_duplicated_keys<TypePairs...>(),
"Compile time type map got duplicate key values.");

template <typename QueryType>
using find_type = typename decltype(get_pair_instance(
std::is_same<QueryType, QueryType>{}))::value;
};

// Define compile-time type maps.
using TypePair0 = TypePair<int16_t, uint16_t>;
using TypePair1 = TypePair<int32_t, uint32_t>;
using TypePair2 = TypePair<int64_t, uint64_t>;
using TypePair3 = TypePair<int64_t, uint64_t>;

using TypeMapIntToUInt0 = TypeMap<TypePair0>;
using TypeMapIntToUInt1 = TypeMap<TypePair0, TypePair1>;
using TypeMapIntToUInt2 = TypeMap<TypePair0, TypePair1, TypePair2>;
// Unable to compile because of the has_duplicated_keys static_assert.
// TypePair2 and TypePair3 have the same key type.
// using TypeMapIntToUInt3 = TypeMap<TypePair0, TypePair1, TypePair2,
// TypePair3>;

int main()
{
static_assert(
std::is_same_v<TypeMapIntToUInt0::find_type<int16_t>, uint16_t>);
static_assert(
std::is_same_v<TypeMapIntToUInt1::find_type<int16_t>, uint16_t>);
static_assert(
std::is_same_v<TypeMapIntToUInt1::find_type<int32_t>, uint32_t>);
static_assert(
std::is_same_v<TypeMapIntToUInt2::find_type<int16_t>, uint16_t>);
static_assert(
std::is_same_v<TypeMapIntToUInt2::find_type<int32_t>, uint32_t>);
static_assert(
std::is_same_v<TypeMapIntToUInt2::find_type<int64_t>, uint64_t>);
// Unable to compile because the key does not exist in the type map.
// static_assert(
// std::is_same<TypeMapIntToUInt2::find_type<int8_t>, uint64_t>::value,
// "");
}

To build and run the program, please run the following commands.

1
2
$ g++ generic_compile_time_type_map.cpp -o generic_compile_time_type_map --std c++17
$ ./generic_compile_time_type_map

The implementation used C++ 17 features such as fold expressions, pack expansion in using-declaration, and std::is_same_v. Those features are not available in C++ 14 but may have workarounds. However, we will not discuss these in this article.

References

Author

Lei Mao

Posted on

12-22-2024

Updated on

12-22-2025

Licensed under


Comments