custom minimalist std::map with constexpr operator[]

498 Views Asked by At

I am currently working on implementing a map container in C++, which should be able to function as a compile-time constant. More specifically, my intention is to create a static, pre-defined lookup table. In this table, a key-value pair should evaluate to its corresponding value, during the compilation. If a key is not present, a compile-time error should be thrown.

To illustrate, I am aiming to create a map similar to the following:

map m{
            {"pi", 3.14},
            {"e", 2.71828}
}

I want m["pi"] to evaluate to 3.14 during compilation, while m["gamma"] should produce a compile-time error. The aim here is not only to avoid the necessity of runtime computations, but also to make the code more clear by communicating that the map is a predefined static lookup table, and not something that will change dynamically during runtime.

After a day of hacking, I got the following sniplet:

#include <array>
#include <iostream>
#include <algorithm>

template<typename Key, typename Value, size_t Size>
struct map {
    using MapType = std::pair<Key, Value>;
    std::array<MapType, Size> data;

    constexpr map(std::initializer_list<MapType> init) : data{} {
        std::copy(init.begin(), init.end(), data.begin());
    }

    constexpr Value operator[](const Key& key) const {
        auto it = std::find_if(data.begin(), data.end(), [&](const MapType& pair) {
            return pair.first == key;
        });

        if (it == data.end())
            throw std::out_of_range("Key not found in map");

        return it->second;
    }
};

int main() {
    constexpr map<int, int, 1> m1 {
            {1, 2}
    };

    std::cout << m1[1] << std::endl;

    constexpr map<const char*, float, 2> m2 {
            {"pi", 3.14f},
            {"e", 2.71828f}
    };

    std::cout << m2["pi"] << std::endl;
    // The line below will raise a runtime error "Key not found in map"
    std::cout << m2["gamma"] << std::endl;
}

While the above implementation is operational to a certain extent, it has a couple of issues:

  • The size needs to be specified in the template which, ideally, I would prefer to avoid.

  • The map throws a runtime error for non-existent keys like m2["gamma"], when my objective is to enforce this as a compile-time error.

Any suggestions or pointers to enhance the implementation in the context of these issues would be highly appreciated. Thanks!


Addendum: Per suggestion from @康桓瑋, operator[] can be declared as consteval, like this:

consteval Value operator[](const Key& key) const {
    for (auto i: data) {
        if  (std::get<0>(i)==key) {
            return std::get<1>(i);
        }
    }
}

This correctly fails to compile when a non-existent key is used, although the error message does not provide relevant information. compiles on C++20+

2

There are 2 best solutions below

2
Useless On

This is a quick sketch of a completely static map, assuming you have C++20 floating-point non-type template parameter support:

template <double Val> struct ValueWrapper
{
    constexpr operator double () const noexcept { return Val; }
};

struct Pi_Key : ValueWrapper<3.14> {};
struct E_Key : ValueWrapper<2.71828> {};

using ConstantsMap = std::tuple<Pi_Key, E_Key>;

template <typename Key>
constexpr double get_constant() {
    return std::get<Key>(ConstantsMap{});
}

int main()
{
    return static_cast<int>(
        (get_constant<Pi_Key>() - get_constant<E_Key>()) * 100.0
    );
}

If you don't have floating-point non-type template parameters yet, you still need a distinct type for each constant, but now it has to actually store a member of type double (and you need an actual tuple instance storing those objects). Once you have that, the lookup is essentially the same:

struct ValWrapper
{
    double val;
};

struct Pi_K : ValWrapper { constexpr Pi_K() : ValWrapper{3.14} {} };
struct E_K : ValWrapper { constexpr E_K() : ValWrapper{2.71828} {} };

constexpr std::tuple<Pi_K, E_K> konstants;

template <typename Key>
constexpr double get_konstant() {
    return std::get<Key>(konstants).val;
}
0
user23952 On

After some hacking, I was able to work out something that seems usable. Instead of a custom map implementation, I used a c-style array of key-value pairs, which is apparently the best thing that can be implemented as a constexr. Instead of implementing operator[], I made a function GetValue, which takes in the above-mentioned c-style array, and looks up the corresponding value.

This works, but I feel it could be more elegant. Suggestions for improvement are very welcome! The code is below.

#include <iostream>
#include <algorithm>

// A constexpr function to compare if two c-string are equal
constexpr bool strcmp(const char* str1, const char* str2) {
    while (*str1 && (*str1 == *str2)) {
        ++str1;
        ++str2;
    }
    return (*str1 - *str2) == 0;
}

// A structure representing a key-value pair
template <typename ValueType>
struct KeyValuePair {
    const char* key;
    const ValueType value;
};

// A function to get a value from an array of KeyValuePair given a key
template <typename ValueType, size_t N>
constexpr const ValueType& GetValue(const KeyValuePair<ValueType>(&data)[N], const char* key) {
    auto found = std::find_if(data, data + N, [key](const auto& kvPair) {
        return strcmp(kvPair.key, key);
    });

    if (found == std::end(data)) {
        throw std::exception();
    }

    return found->value;
}

int main() {
    using ValueType = double;
    constexpr KeyValuePair<ValueType> keyValueData[]{
            {"pi", 3.14},
            {"e", 2.71828},
            {"one", 1},
            {"two", 2}
    };

    constexpr auto piValue = GetValue(keyValueData, "pi");
    std::cout << piValue;

    // The line below causes a compilation error if uncommented, which is the desired behaviour.
    //constexpr auto nonexoistant = GetValue(keyValueData, "c");
    return 0;
}
``