Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Initialize constant string array out of order [closed]

Tags:

c++

constants

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..

like image 697
qdot Avatar asked Sep 08 '26 11:09

qdot


2 Answers

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).

like image 99
Nick Matteo Avatar answered Sep 10 '26 02:09

Nick Matteo


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.

like image 31
Jan Schultke Avatar answered Sep 10 '26 01:09

Jan Schultke



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!