6

Trying to increase robustness of a piece of embedded code - simplified like this:

enum tags { 
  TAG1, 
  TAG2, 
  NUM_TAGS
}; 

const char* const vals_ok[] = { "val1", "val2" }; 

// Doesn't work (I understand why, but I look for ideas how to get it working again)
const char** const vals = []() constexpr {
  const char* rval[NUM_TAGS] = {0};
  rval[TAG2] = "val2";
  rval[TAG1] = "val1";
  return rval;
}();

I'm trying to ensure that the tags and values don't get out of sync with one another, if someone forgets to add a value in one but not the other place.

Since it's an embedded system, I want it to be a const-of-const array, reordered at compile-time - essentially I want the compiler to come up with the exact same output as in vals_ok - but with added maintainability.

It "works" with declaring the inner lambda variable rval static - but lands in .bss with a runtime copy initializer..

13
  • 5
    Would std::array also do? Because that can be returned. And it should not have any overhead compared to a raw array. Commented Apr 29 at 19:37
  • How are vals_ok and vals related? Is there supposed to be a lookup in vals_ok somehow? Can't you just verify that there are NUM_TAGS elements in vals_ok? static_assert(std::size(vals_ok) == NUM_TAGS); Commented Apr 29 at 20:08
  • 2
    Reordered how? Please describe the reordering rules. Right now it looks like you just put hardcoded values in rval at the same places as you have them in vals_ok. Commented Apr 29 at 20:14
  • @TedLyngmo: I take it that OP means, in the event a new tag is added between TAG1 and TAG2, with the array in vals_ok that just depends on position the string "val2" would now be associated with the wrong tag. OP wants to explicitly assign strings to tags so that this doesn't happen Commented Apr 29 at 20:22
  • @NickMatteo Perhaps, and wouldn't just checking if there are the same number of elements solve that? Who'd be able to tell if they are placed in the wrong order? Commented Apr 29 at 20:26

4 Answers 4

7

As suggested in comments, std::array should make this easier to write:

constexpr std::array vals = []()
{
    std::array<const char*, NUM_TAGS> rval{};
    rval[TAG2] = "val2";
    rval[TAG1] = "val1";
    return rval;
}();

On godbolt

I'm not sure if this really ensures that your values and names don't get out of sync, but you can add a static_assert that you didn't miss any, like static_assert(!std::ranges::contains(vals, nullptr)); (example on godbolt; example when you forgot a tag).

Sign up to request clarification or add additional context in comments.

4 Comments

What do you mean by "out of sync"?
@DrewDormann updating the enum values without updating the array.
And especially in a hard-to-track way where you enter the enum value in the middle, shifting the indexes - but not in the middle of the array. This approach also allows the use of static_assert to ensure there are no nulls in the array..
The constexpr on the lambda is unnecessary.
2

I'm trying to ensure that the tags and values don't get out of sync with one another, if someone forgets to add a value in one but not the other place.

"Idiot-proofing" code like that would be tricky if the strings were defined in a different place than the tags. This is a good use of X macros, like:

// All entries are managed here:

#define TAG_ENUM_DATA(F) \
    F(TAG1, "val1") \
    F(TAG2, "val2")

// Macro-expanded, do not touch:

#define TAG_ENUM_ENUMERATOR(tag, str) tag,
#define TAG_ENUM_STRING(tag, str) str,

enum {
    // expands to: TAG1, TAG2,
    TAG_ENUM_DATA(TAG_ENUM_ENUMERATOR)
    NUM_TAGS
};

constexpr const char* vals_ok[] = {
    // expands to "val1", "val2",
    TAG_ENUM_DATA(TAG_ENUM_STRING)
};

constexpr const char* vals[] = {
    // expands to "val1", "val2",
    TAG_ENUM_DATA(TAG_ENUM_STRING)
    // one extra entry corresponding to NUM_TAGS
    nullptr
};

When someone wants to add a new entry or modify an existing one, they would just be modifying the TAG_ENUM_DATA macro; the enum and array are generated from that data.


Note that modern, idiomatic C++ should probably use std::array instead of plain arrays, std::string_view instead of const char*, and enum class instead of enum. I assume you have good reasons to avoid these things, seeing that this is embedded.

1 Comment

X-macros are such a good idea! I should really keep a note of "cool stuff I saw before, consult before thinking"! Actually std::array (to my great surprise compiled to .rodata const with g++-14 - I was worried the libstdc++ would try to do something at runtime but all good!) works for this particular usecase, but X-macros would be my pick in case I wanted to support plain-C.
1

For an embedded system you can use an array of struct so that the data can be placed into the constant data section of your application.

struct Record
{
    enum tags tag_id;
    char * text;
};

IMHO, a better record would have a fixed width text field.

For example:

const Record enum_table[] =
{
    {tag1, "val1"},
    {tag2, "val2"},
    //...
};

The actual array allows you to place the data into a read-only section, and access the data directly. Other data structures may need to be constructed at runtime.

1 Comment

I wish I could do that, the underlying LWIP library expects them in columnar order. But the std::array actually generates correct code in .rodata!
1

It is possible to define some map class usable for constexpr objects. It is then possible to define your mapping in a natural way as follows

constexpr CTMap<tags,const char*,2> enum_table
{{{
    {tags::TAG2, "val2" },
    {tags::TAG1, "val1" }
}}};

where CTMap is defined by

template <class Key, class Value, int N>
struct CTMap
{
    using KV = std::pair<Key,Value>;
    std::array<KV,N> pairs;

    constexpr Value  at (Key key) const {
        for (int i=0; i<N; i++)  {
           if (std::get<0>(pairs[i])==key)  { return std::get<1>(pairs[i]); }
        }
        throw std::out_of_range ("key not found");
    }

    template<std::size_t I>
    constexpr auto as_array() const {
        return [this] <std::size_t...Is> (std::index_sequence<Is...>)  {
            return std::array<std::tuple_element_t<I,KV>,N> {std::get<I>(pairs[Is])...} ;
        } (std::make_index_sequence<N>());
    }

    constexpr auto keys  () const { return as_array<0>(); }
    constexpr auto values() const { return as_array<1>(); }
};

Even if one defines such a map by a list of pairs, it is possible to retrieve a constexpr array holding the keys with the CTMap::keys method (the same for the values)

static_assert (std::is_same_v<decltype(enum_table.keys()),std::array<tags,2>>);
static_assert (std::size(enum_table.keys())==2);
static_assert (enum_table.keys()[0] == tags::TAG2);
static_assert (enum_table.keys()[1] == tags::TAG1);

Demo

Note that this has limitations since the lookup in CTMap::at is done in linear time and therefore should be used for a rather low number of map entries.


Update

I forgot the point that the OP wanted to reorder the entries. This can be done by getting the indexes permutation of the keys and then use it while computing the keys and values arrays

    constexpr auto getOrderedIndexes() const {
        // We get the keys as a std::array
        auto keys = [this] <std::size_t...Is> (std::index_sequence<Is...>)  {
            return std::array<Key,N> {std::get<0>(pairs[Is])...} ;
        } (std::make_index_sequence<N>());
        // We look for the indexes permutation that sorts the keys.
        std::array<std::size_t,N> indexes = {};
        std::iota (std::begin(indexes), std::end(indexes),0);
        std::ranges::sort(indexes, [&keys] (auto i, auto j) { return keys[i]<keys[j]; });
        return indexes;
    }

    template<std::size_t I>
    constexpr auto as_array() const {
        return [this] <std::size_t...Is> (std::index_sequence<Is...>)  {
            // We use the indexes permutation here.
            return std::array<std::tuple_element_t<I,KV>,N> {std::get<I>(pairs[getOrderedIndexes()[Is]])...} ;
        } (std::make_index_sequence<N>());
    }

With the reordering, we have now for instance

static_assert (std::is_same_v<decltype(enum_table.keys()),std::array<tags,2>>);
static_assert (std::size(enum_table.keys())==2);
static_assert (enum_table.keys()[0] == tags::TAG1);
static_assert (enum_table.keys()[1] == tags::TAG2);

Demo

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.