Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's a good way to store a small, fixed size, hierarchical set of static data?

Tags:

c++

containers

I'm looking for a way to store a small multidimensional set of data which is known at compile time and never changes. The purpose of this structure is to act as a global constant that is stored within a single namespace, but otherwise globally accessible without instantiating an object.

If we only need one level of data, there's a bunch of ways to do this. You could use an enum or a class or struct with static/constant variables:

class MidiEventTypes{
   public:
   static const char NOTE_OFF = 8;
   static const char NOTE_ON = 9;
   static const char KEY_AFTERTOUCH = 10;
   static const char CONTROL_CHANGE = 11;
   static const char PROGRAM_CHANGE = 12;
   static const char CHANNEL_AFTERTOUCH = 13;
   static const char PITCH_WHEEL_CHANGE = 14;
};

We can easily compare a numeric variable anywhere in the program by using this class with it's members:

char nTestValue = 8;
if(nTestValue == MidiEventTypes::NOTE_OFF){} // do something...

But what if we want to store more than just a name and value pair? What if we also want to store some extra data with each constant? In our example above, let's say we also want to store the number of bytes that must be read for each event type.

Here's some pseudo code usage:

char nTestValue = 8;
if(nTestValue == MidiEventTypes::NOTE_OFF){
   std::cout << "We now need to read " << MidiEventTypes::NOTE_OFF::NUM_BYTES << " more bytes...." << std::endl;
}

We should also be able to do something like this:

char nTestValue = 8;
// Get the number of read bytes required for a MIDI event with a type equal to the value of nTestValue.
char nBytesNeeded = MidiEventTypes::[nTestValue]::NUM_BYTES; 

Or alternatively:

char nTestValue = 8;    
char nBytesNeeded = MidiEventTypes::GetRequiredBytesByEventType(nTestValue);

and:

char nBytesNeeded = MidiEventTypes::GetRequiredBytesByEventType(NOTE_OFF);

This question isn't about how to make instantiated classes do this. I can do that already. The question is about how to store and access "extra" constant (unchanging) data that is related/attached to a constant. (This structure isn't required at runtime!) Or how to create a multi-dimensional constant. It seems like this could be done with a static class, but I've tried several variations of the code below, and each time the compiler found something different to complain about:

static class MidiEventTypes{
   
   public:
   static const char NOTE_OFF = 8;
   static const char NOTE_ON = 9;
   static const char KEY_AFTERTOUCH = 10; // Contains Key Data
   static const char CONTROL_CHANGE = 11; // Also: Channel Mode Messages, when special controller ID is used.
   static const char PROGRAM_CHANGE = 12;
   static const char CHANNEL_AFTERTOUCH = 13;
   static const char PITCH_WHEEL_CHANGE = 14;
   
   // Store the number of bytes required to be read for each event type.
   static std::unordered_map<char, char> BytesRequired = {
      {MidiEventTypes::NOTE_OFF,2},
      {MidiEventTypes::NOTE_ON,2},
      {MidiEventTypes::KEY_AFTERTOUCH,2},
      {MidiEventTypes::CONTROL_CHANGE,2},
      {MidiEventTypes::PROGRAM_CHANGE,1},
      {MidiEventTypes::CHANNEL_AFTERTOUCH,1},
      {MidiEventTypes::PITCH_WHEEL_CHANGE,2},
   };
   
   static char GetBytesRequired(char Type){
      return MidiEventTypes::BytesRequired.at(Type);
   }
   
};

This specific example doesn't work because it won't let me create a static unordered_map. If I don't make the unordered_map static, then it compiles but GetBytesRequired() can't find the map. If I make GetBytesRequired() non-static, it can find the map, but then I can't call it without an instance of MidiEventTypes and I don't want instances of it.

Again, this question isn't about how to fix the compile errors, the question is about what is the appropriate structure and design pattern for storing static/constant data that is more than a key/value pair.

These are the goals:

  • Data and size is known at compile time and never changes.

  • Access a small set of data with a human readable key to each set. The key should map to a specific, non-linear integer.

  • Each data set contains the same member data set. ie. Each MidiEventType has a NumBytes property.

  • Sub-items can be accessed with a named key or function.

  • With the key, (or a variable representing the key's value), we should be able to read extra data associated with the constant item that the key points to, using another named key for the extra data.

  • We should not need to instantiate a class to read this data, as nothing changes, and there should not be more than one copy of the data set.

  • In fact, other than an include directive, nothing should be required to access the data, because it should behave like a constant.

  • We don't need this object at runtime. The goal is to make the code more organized and easier to read by storing groups of data with a named label structure, rather than using (ambiguous) integer literals everywhere.

  • It's a constant that you can drill down into... like JSON.

  • Ideally, casting should not be required to use the value of the constant.

  • We should avoid redundant lists that repeat data and can get out of sync. For example, once we define that NOTE_ON = 9, The literal 9 should not appear anywhere else. The label NOTE_ON should be used instead, so that the value can be changed in only one place.

  • This is a generic question, MIDI is just being used as an example.

  • Constants should be able to have more than one property.

What's the best way to store a small, fixed size, hierarchical (multidimensional) set of static data which is known at compile time, with the same use case as a constant?

like image 341
Nick Avatar asked Sep 06 '21 09:09

Nick


People also ask

What is static data store?

Static data (aka Code, Lookup, List or Reference data), in the context of a relational database management system, is generally data that represents fixed data, that generally doesn't change, or if it does, changes infrequently, such as abbreviations for US states for example e.g. ME, MA, NC.

Should static data be in a database?

Most databases also require what is often referred to as 'static' or 'reference' data, which will include such things as error messages, names of geographical locations, currency names, or tax information. This data must be in place before the database can be used in any effective way.

What is data structure in C++ and its types?

C++ Structure The structure is a user-defined data type. It works similarly like arrays. Structures help you in grouping items of different types in a single group. It stores the collection of different data types.


2 Answers

Here is my take on it, a full constexpr compile time solution. For your use also put the midi stuff in a header file and you're good to go.

With header files https://www.onlinegdb.com/lGp7zMNB6

#include <iostream>
#include "const_string.h"
#include "const_map.h"

namespace midi
{
    using data_t = char;
    using string_t = const_string<32>; // 32 is big enough to hold strings in map

    namespace control
    {
        constexpr data_t NOTE_OFF = 8;
        constexpr data_t NOTE_ON = 9;
        constexpr data_t KEY_AFTERTOUCH = 10;
        constexpr data_t CONTROL_CHANGE = 11;
        constexpr data_t PROGRAM_CHANGE = 12;
        constexpr data_t CHANNEL_AFTERTOUCH = 13;
        constexpr data_t PITCH_WHEEL_CHANGE = 14;
    } /* namespace control */

    constexpr auto required_bytes = make_const_map<data_t, data_t>({
        {control::NOTE_OFF,2},
        {control::NOTE_ON,2},
        {control::KEY_AFTERTOUCH,2},
        {control::CONTROL_CHANGE,2},
        {control::PROGRAM_CHANGE,1},
        {control::CHANNEL_AFTERTOUCH,1},
        {control::PITCH_WHEEL_CHANGE,2}
    });

    constexpr auto str = make_const_map<data_t, string_t>({
        { control::NOTE_ON,"Note on" },
        { control::NOTE_OFF,"Note off" },
        { control::CONTROL_CHANGE, "Control change"},
        { control::CHANNEL_AFTERTOUCH, "Channel aftertouch"},
        { control::PITCH_WHEEL_CHANGE, "Pitch wheel change"}
    });

} /* namespace midi */

int main()
{
    static_assert(midi::control::NOTE_OFF == 8, "test failed");
    static_assert(midi::required_bytes[midi::control::NOTE_OFF] == 2, "test failed");
    static_assert(midi::required_bytes[13] == 1, "test failed");
    static_assert(midi::str[midi::control::NOTE_OFF] == "Note off", "test failed");

    return 0;
}

// Edit after acceptance : cleaner syntax

#include <iostream>
#include "const_string.h"
#include "const_map.h"

namespace midi_details
{
    using data_t = char;
    using string_t = const_string<32>;
}

constexpr midi_details::data_t MIDI_NOTE_OFF = 8;
constexpr midi_details::data_t MIDI_NOTE_ON = 9;
constexpr midi_details::data_t MIDI_KEY_AFTERTOUCH = 10;
constexpr midi_details::data_t MIDI_CONTROL_CHANGE = 11;
constexpr midi_details::data_t MIDI_PROGRAM_CHANGE = 12;
constexpr midi_details::data_t MIDI_CHANNEL_AFTERTOUCH = 13;
constexpr midi_details::data_t MIDI_PITCH_WHEEL_CHANGE = 14;

namespace midi_details
{
    constexpr auto required_bytes = make_const_map<data_t, data_t>({
        {MIDI_NOTE_OFF,2},
        {MIDI_NOTE_ON,2},
        {MIDI_KEY_AFTERTOUCH,2},
        {MIDI_CONTROL_CHANGE,2},
        {MIDI_PROGRAM_CHANGE,1},
        {MIDI_CHANNEL_AFTERTOUCH,1},
        {MIDI_PITCH_WHEEL_CHANGE,2}
        });

    constexpr auto str = make_const_map<data_t, string_t>({
            { MIDI_NOTE_ON,"Note on" },
            { MIDI_NOTE_OFF,"Note off" },
            { MIDI_CONTROL_CHANGE, "Control change"},
            { MIDI_CHANNEL_AFTERTOUCH, "Channel aftertouch"},
            { MIDI_PITCH_WHEEL_CHANGE, "Pitch wheel change"}
        });

    struct info_t
    {
        constexpr info_t(data_t r, string_t n) :
            required_bytes{ r },
            name{ n }
        {
        }

        data_t  required_bytes;
        string_t name;
    };

} /* namespace midi_details */

constexpr auto midi(midi_details::data_t value)
{
    return midi_details::info_t{ midi_details::required_bytes[value], midi_details::str[value] };
}

int main()
{
    static_assert(MIDI_NOTE_OFF == 8);
    static_assert(midi(MIDI_NOTE_OFF).required_bytes == 2, "test failed");
    static_assert(midi(MIDI_NOTE_OFF).name == "Note off", "test failed");

    return 0;
}
like image 189
Pepijn Kramer Avatar answered Sep 28 '22 05:09

Pepijn Kramer


How about something like:

struct MidiEventType
{
    char value;
    char byteRequired; // Store the number of bytes required to be read
};

struct MidiEventTypes{
   static constexpr MidiEventType NOTE_OFF { 8, 2};
   static constexpr MidiEventType NOTE_ON { 9, 2};
   static constexpr MidiEventType KEY_AFTERTOUCH { 10, 2};
   static constexpr MidiEventType CONTROL_CHANGE { 11, 2};
   static constexpr MidiEventType PROGRAM_CHANGE  { 12, 1};
   static constexpr MidiEventType CHANNEL_AFTERTOUCH { 13, 1};
   static constexpr MidiEventType PITCH_WHEEL_CHANGE { 14, 2};
};
like image 34
Jarod42 Avatar answered Sep 28 '22 06:09

Jarod42