Home > Software engineering >  Captured in-line array in constexpr variable gets lost on runtime
Captured in-line array in constexpr variable gets lost on runtime

Time:04-05

I'm trying to create an [int/enum]-to-text mapping class with as little overhead as possible for it's users. It's constructor should be passed a list of value-to-text mappings, which can be queried afterwards. Created objects should be constexpr and have an optional size argument, which allows the compiler to optionally check at compile time if the number of passed mappings matches what is expected. This is especially useful as an extra safety measure when used with enums -- that is: this way you can force a compile error if you neglect to add a mapping for newly added enum values. It should work with C 14 under Visual Studio 2019 and Xcode 9 and 12.

My current stab at this is the code below. However, under Visual Studio 2019 at least, the passed array of mappings is not correctly captured in the m_mappings member variable. When you run this code, m_mappings points to a random memory address, so any output you get is wrong (if it doesn't outright crash).

#include <iostream>


template <typename Type>
struct Mapping {
    Type value;
    const char* text;
};


template <typename Type, Type maxValue = Type(-1)>
class Mapper {
public:
    template <size_t numMappings>
    explicit constexpr Mapper(const Mapping<Type>(&mappings)[numMappings]) :
        m_mappings(mappings),
        m_numMappings(numMappings) {
        static_assert(
            int(maxValue) == -1 || numMappings == int(maxValue)   1,
            "Some mappings are missing!"
        );
    }

    const char* Map(Type value) const {
        for (size_t mappingNr = 0; mappingNr < m_numMappings;   mappingNr)
            if (m_mappings[mappingNr].value == value)
                return m_mappings[mappingNr].text;

        return "?";
    }

private:
    const Mapping<Type>* m_mappings;
    const size_t m_numMappings;
};


enum class TestEnum {
    a,
    b,
    c,

    maxValue = c
};


int main() {
    constexpr Mapper<int> intMapper_noCheck({
        { 11, "a" },
        { 5, "b" },
        { 26, "x" }
    });
    std::cout << intMapper_noCheck.Map(10);

    constexpr Mapper<int, 3> intMapper_check({
        { 0, "z" },
        { 1, "f" },
        { 2, "t" },
        { 3, "#" }
    });
    std::cout << intMapper_check.Map(2);

    // if we'd pass e.g. TestEnum::b here, we get a nice compile time error
    constexpr Mapper<TestEnum, TestEnum::maxValue> enumMapper({
        { TestEnum::a, "-" },
        { TestEnum::b, "-" },
        { TestEnum::c, " " }
    });
    std::cout << enumMapper.Map(TestEnum::b);

    // expected output by now: "?t-"

    std::cin.get();
    return 0;
}

A possible solution is to capture the mapping array in a separate constexpr variable, and pass that to the mapper objects, like so:

    constexpr Mapping<TestEnum> enumMapping[] = {
        { TestEnum::a, "-" },
        { TestEnum::b, "-" },
        { TestEnum::c, " " }
    };
    constexpr Mapper<TestEnum, TestEnum::maxValue> enumMapper(enumMapping);
    std::cout << enumMapper.Map(TestEnum::b);

This way the mappings do get preserved and the output is correct. However, I find this extra layer makes it much more 'messy'...

The complicating factor here is that the passed in array's size must be captured in a constexpr-friendly way, and I do not want to have to specify it separately by hand.

  • Using a fixed size array with the size specified in the constructor's template argument is one way of doing it, but when passing an array in-line it is thus not accessible at runtime.
  • Another version would be to pass (and store) a std::initializer_list, but you can't static_assert on it's size method. As a workaround we could also initialize m_mappings like m_mappings(sizeMatches? mappings: throw "mismatch!"), but that way non-constexpr objects may perform nasty throws at runtime (and unfortunately the rest of the code base isn't exactly exception safe).
  • I contemplated using a std::array instead (that one's size is constexpr accessible), but then there is no way to pass the used size to m_mappings (the size is only known to the constructor template, and not to the class template).
  • I also contemplated using a template parameter pack for the constructor so users can pass the loose value-and-text args two-by-two. But then how do I stuff these into m_mappings? I'd have to e.g. make m_mappings a std::vector to do so, but that's not constexpr-compatible.

What could be another option whilst keeping it nice and clean like in my first version?

CodePudding user response:

The reason it fails is that you store a pointer to the array passed to the constructor for later use.

But in your compact cases, that array no longer exists after the constructor has run. So you need to do some allocation (which might not be constexpr-friendly in all compilers) and copying. The fact that you use a char* instead of a std::string further adds to this problem - there too there is no guarantee that the pointed-to string still exists when you use it.

Also, unless this is for some sort of coursework, consider looking at the constexpr implementations of sets and maps provided by https://github.com/serge-sans-paille/frozen

CodePudding user response:

After some more tampering I came up with a solution where I just copy over the passed mappings into a plain old array member. I also tried to use an std::array, but it just isn't constexpr-friendly enough in C 14.

What I tried previously was to automatically capture the mapping list size in the templated constructor (numMappings is deduced by the compiler), and then match it to the specified expected number of mappings from the class template (maxValue). But now the class template itself needs to know the number of mappings we're going to pass, so that it can reserve storage for the copy. So I've also repurposed the maxValue parameter to represent exactly that.

A drawback is thus that you now always need to count out manually how many mappings you're going to pass, which is a pain when mapping over large discontinuous int ranges where you'd really like not to care about that detail. For enums nothing really changes though, and I mainly wrote this class for handling enums.

So it's not a 100% perfect fit with the question, but it'll do I suppose... ah well.

#include <iostream>


template <typename Type>
struct Mapping {
    Type value;
    const char* text;
};


template <typename Type, Type maxValue>
class Mapper {
public:
    template <size_t numMappings>
    explicit constexpr Mapper(const Mapping<Type>(&mappings)[numMappings]) :
        m_mappings{} {
        constexpr int correctNumMappings{ int(maxValue)   1 };

        static_assert(numMappings <= correctNumMappings, "Too many mappings given!");
        static_assert(numMappings >= correctNumMappings, "Some mappings are missing!");

        for (size_t mappingNr = 0; mappingNr < numMappings;   mappingNr)
            m_mappings[mappingNr] = mappings[mappingNr];
    }

    const char* Map(Type value) const {
        for (const Mapping<Type>& mapping : m_mappings)
            if (mapping.value == value)
                return mapping.text;

        return "?";
    }

private:
    Mapping<Type> m_mappings[int(maxValue)   1];
};


enum class TestEnum {
    a,
    b,
    c,

    maxValue = c
};


int main() {
    constexpr Mapper<int, 3> intMapper_check({
        { 0, "z" },
        { 1, "f" },
        { 2, "t" },
        { 3, "#" }
    });
    std::cout << intMapper_check.Map(2) << "\n";

    constexpr Mapper<TestEnum, TestEnum::maxValue> enumMapper({
        { TestEnum::a, "-" },
        { TestEnum::b, "-" },
        { TestEnum::c, " " }
    });
    std::cout << enumMapper.Map(TestEnum::b) << "\n";

    std::cin.get();
    return 0;
}
  • Related