Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compile-Time Lookup-Table with initializer_list

Tags:

c++

c++17

Suppose you have some hash values and want to map them to their respective strings at compile time. Ideally, I'd love to be able to write something along the lines of:

constexpr std::map<int, std::string> map = { {1, "1"}, {2 ,"2"} };

Unfortunately, this is neither possible in C++17 nor C++2a. Nevertheless, I tried emulating this with std::array, but can't get the size of the initializer list at compile time to actually set the type of the array correctly without explicitly specifying the size. Here is my mockup:


template<typename T0, typename T1>
struct cxpair
{
    using first_type = T0;
    using second_type = T1;

    // interestingly, we can't just = default for some reason...
    constexpr cxpair()
        : first(), second()
    {  }

    constexpr cxpair(first_type&& first, second_type&& second)
        : first(first), second(second)
    {  }

    // std::pair doesn't have these as constexpr
    constexpr cxpair& operator=(cxpair<T0, T1>&& other)
    { first = other.first; second = other.second; return *this; }

    constexpr cxpair& operator=(const cxpair<T0, T1>& other)
    { first = other.first; second = other.second; return *this; }

    T0 first;
    T1 second;
};

template<typename Key, typename Value, std::size_t Size = 2>
struct map
{
    using key_type = Key;
    using mapped_type = Value;
    using value_type = cxpair<Key, Value>;

    constexpr map(std::initializer_list<value_type> list)
        : map(list.begin(), list.end())
    {  }

    template<typename Itr>
    constexpr map(Itr begin, const Itr &end)
    {
        std::size_t size = 0;
        while (begin != end) {
            if (size >= Size) {
                throw std::range_error("Index past end of internal data size");
            } else {
                auto& v = data[size++];
                v = std::move(*begin);
            }
            ++begin;
        }
    }

    // ... useful utility methods omitted

private:
    std::array<value_type, Size> data;
    // for the utilities, it makes sense to also have a size member, omitted for brevity
};

Now, if you just do it with plain std::array things work out of the box:


constexpr std::array<cxpair<int, std::string_view>, 2> mapp = {{ {1, "1"}, {2, "2"} }};

// even with plain pair
constexpr std::array<std::pair<int, std::string_view>, 2> mapp = {{ {1, "1"}, {2, "2"} }};

Unfortunately, we have to explicitly give the size of the array as second template argument. This is exactly what I want to avoid. For this, I tried building the map you see up there. With this buddy we can write stuff such as:

constexpr map<int, std::string_view> mapq = { {1, "1"} };
constexpr map<int, std::string_view> mapq = { {1, "1"}, {2, "2"} };

Unfortunately, as soon as we exceed the magic Size constant in the map, we get an error, so we need to give the size explicitly:

//// I want this to work without additional shenanigans:
//constexpr map<int, std::string_view> mapq = { {1, "1"}, {2, "2"}, {3, "3"} };
constexpr map<int, std::string_view, 3> mapq = { {1, "1"}, {2, "2"}, {3, "3"} };

Sure, as soon as you throw in the constexpr scope, you get a compile error and could just tweak the magic constant explicitly. However, this is an implementation detail I'd like to hide. The user should not need to deal with these low-level details, this is stuff the compiler should infer.

Unfortunately, I don't see a solution with the exact syntax map = { ... }. I don't even see light for things like constexpr auto map = make_map({ ... });. Besides, this is a different API from the runtime-stuff, which I'd like to avoid to increase ease of use.

So, is it somehow possible to infer this size parameter from an initializer list at compile time?

like image 840
NaCl Avatar asked Oct 11 '25 12:10

NaCl


1 Answers

std::array has a deduction guide:

template <class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;

which lets you write:

// ok, a is array<int, 4>
constexpr std::array a = {1, 2, 3, 4};

We can follow the same principle and add a deduction guide for map like:

template <typename Key, typename Value, std::size_t Size>
struct map {
    constexpr map(std::initializer_list<std::pair<Key const, Value>>) { }
};

template <class T, class... U>
map(T, U...) -> map<typename T::first_type, typename T::second_type, sizeof...(U)+1>;

Which allows:

// ok, m is map<int, int, 3>
constexpr map m = {std::pair{1, 1}, std::pair{1, 2}, std::pair{2, 3}};

Unfortunately, this approach requires naming each type in the initializer list - you can't just write {1, 2} even after you wrote pair{1, 1}.


A different way of doing it is to take an rvalue array as an argument:

template <typename Key, typename Value, std::size_t Size>
struct map {
    constexpr map(std::pair<Key, Value>(&&)[Size]) { }
};

Which avoids having to write a deduction guide and lets you only have to write the type on the first one, at the cost of an extra pair of braces or parens:

// ok, n is map<int, int, 4>
constexpr map n{{std::pair{1, 1}, {1, 2}, {2, 3}, {3, 4}}};

// same
constexpr map n({std::pair{1, 1}, {1, 2}, {2, 3}, {3, 4}});

Note that the array is of pair<Key, Value> and not pair<Key const, Value> - which allows writing just pair{1, 1}. Since you're writing a constexpr map anyway, this distinction probably doesn't matter.

like image 97
Barry Avatar answered Oct 14 '25 00:10

Barry