1

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+

user23952
  • 578
  • 3
  • 10
  • Did you thought about unordered_maps? How much big data to be saved in map is? if its too small then I think we are wasting time on wrong place. – Shubham Agrawal Jul 02 '23 at 14:10
  • Seems like a lot of work without any benefit over doing it the straightforward way (with a namespace and constexpr values in that namespace). – Eljay Jul 02 '23 at 14:10
  • 1
    "*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.*" This can be done by declaring your `operator[]` as `consteval`. – 康桓瑋 Jul 02 '23 at 14:28
  • @康桓瑋 yes, consteval is an improvement. Had to rewrite the find_if, because it is not consteval.: consteval Value operator[](const Key& key) const { for (auto i: data) { if (std::get<0>(i)==key) { return std::get<1>(i); } } } – user23952 Jul 02 '23 at 16:42
  • Do you ever need to determine the key to lookup at runtime, or are they actually always known statically as in your example? – Useless Jul 02 '23 at 16:58
  • @Useless they are always statically known at runtime. I could technically replace the keys with values in the code, but that would create a mess since the value type in my application is a composite type. – user23952 Jul 02 '23 at 17:30
  • I'm not sure what you mean about the value, or by statically known at runtime", but creating a compile-time mapping from _type_ to value is as easy as creating distinct wrapper types for the key-value pairs and using a tuple. – Useless Jul 02 '23 at 20:20
  • What did you mean by “known statically” in your first comment? I assumed you implied “known during compile time”. Can you write an example of the solution you outlined? The mapping is from a value to another value. – user23952 Jul 02 '23 at 21:03
  • 1
    As @useless suggests, if you were to put in a distinct type into `operator[]` for each constant, you could trigger an assertion, if the passed key is not present in the map (like in this example: https://godbolt.org/z/xhKh48hfW). This variant is only feasible if the number of constants is rather small and you are willing to name them explicitly. – Quxflux Jul 03 '23 at 08:27
  • @Quxflux, very interesting illustration of advanced C++, like variadic temlates and standard library functions. Unfortunately too complicated for my intended use, I think I'll leave it that. Thanks! – user23952 Jul 03 '23 at 10:51
  • @user23952 You're welcome. In addition to the consteval improvement by 康桓瑋: You could improve the compiler error message spit out by the compiler when the key does not exist like described in [How to fail a consteval function?](https://stackoverflow.com/a/67320663/1130270). Also the map implementation you proposed seems to contain UB when using `const char*` as key. Clang reports this correctly: https://godbolt.org/z/99njq3s3n. A fix would be to use `std::string_view` instead of raw pointers. – Quxflux Jul 03 '23 at 11:17

2 Answers2

1

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;
}
Useless
  • 64,155
  • 6
  • 88
  • 132
  • Thanks @useless, but I have to say that this code looks overly complex. Judging from the answers, there is currently no good way to implement this in C++ – user23952 Jul 06 '23 at 09:40
  • It's shorter than your original, really isn't doing anything complex, and is 100% guaranteed to be all compile time, all the time. You could hide the type definitions in a macro if you prefer, though. – Useless Jul 07 '23 at 10:31
0

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;
}
``
user23952
  • 578
  • 3
  • 10