存储小型、固定大小、分层的静态数据集的好方法是什么?

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

我正在寻找一种方法来存储小型 多维 数据集,这些数据在编译时已知并且永远不会更改。此结构的目的是充当存储在单个名称空间中的全局常量,但可以全局访问而无需实例化对象

如果我们只需要一个级别的数据,有很多方法可以做到这一点。您可以将 enumclassstruct 与 static/constant 变量一起使用:

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;
};

通过使用这个 class 及其成员,我们可以很容易地比较程序中任何地方的数值变量:

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

但是如果我们想要存储的不仅仅是名称和值对呢?如果我们还想用 each 常量存储一些 extra 数据怎么办?在上面的示例中,假设我们还想存储每种事件类型必须读取的字节数。

下面是一些伪代码用法:

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

我们也应该能够做这样的事情:

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; 

或者:

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

和:

char nBytesNeeded = MidiEventTypes::GetRequiredBytesByEventType(NOTE_OFF);

这个问题不是关于如何让 实例化 classes 做到这一点。我已经可以做到了。问题是关于如何存储和访问 related/attached 常量的“额外”常量(不变)数据。 (运行时不需要此结构!)或如何创建多维常量。这似乎可以用静态 class 来完成,但我已经尝试了下面代码的几种变体,每次编译器都会发现一些不同的东西来抱怨:

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);
   }
   
};

这个具体示例不起作用,因为它不允许我创建 static unordered_map。如果我不制作 unordered_map static,那么它会编译,但 GetBytesRequired() 无法 找到 地图。如果我使 GetBytesRequired() 成为非静态的,它可以找到地图,但是如果没有 MidiEventTypes 的实例我就无法调用它并且我不想要它的实例。

同样,这个问题不是关于如何修复编译错误,问题是关于什么是适当的结构和设计模式来存储 static/constant 超过 key/value 的数据对.

这些是目标:

存储编译时已知的小型、固定大小、分层(多维)静态数据集的最佳方法是什么,具有与常量相同的用例?

怎么样:

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};
};

只需编写 constexpr 代码即可访问它。这是我非常混乱的例子:

#include <array>
#include <cstddef>
#include <stdexcept>
#include <iostream>

enum class MidiEvents {
   NOTE_OFF,
   NOTE_ON,
   KEY_AFTERTOUCH,
   CONTROL_CHANGE,
   PROGRAM_CHANGE,
   CHANNEL_AFTERTOUCH,
   PITCH_WHEEL_CHANGE,
   MIDIEVENTS_CNT,
};
constexpr bool operator==(const MidiEvents& a, const char& b) {
    return b == static_cast<char>(a);
}

struct MidiEventType {
    char value;
    char num_bytes; // Store the number of bytes required to be read
    constexpr bool operator==(const char& other) const {
        return value == other;        
    }
    constexpr bool operator==(const MidiEvents& other) const {
        return static_cast<char>(other) == value;
    }
};
constexpr bool operator==(const char& a, const MidiEventType& b) {
    return b == a;
}

struct MidiEventTypes {
    static constexpr std::array<
        MidiEventType, static_cast<size_t>(MidiEvents::MIDIEVENTS_CNT)
    > _data{{
        [static_cast<char>(MidiEvents::NOTE_OFF)] = {8, 2},
        [static_cast<char>(MidiEvents::NOTE_ON)] = {9, 2},
        /* etc.... */
    }};
    static constexpr auto get(char m) {
        for (auto&& i : _data) {
            if (i.value == m) {
                return i;
            }
        }
    }
    static constexpr auto get(MidiEvents m) {
        return _data[static_cast<char>(m)];
    }
    static constexpr auto GetRequiredBytesByEventType(char m) {
        return get(m).num_bytes;
    }
    static constexpr auto GetRequiredBytesByEventType(MidiEvents m) {
        return get(m).num_bytes;
    }
    static constexpr auto NOTE_OFF = _data[static_cast<char>(MidiEvents::NOTE_OFF)];
    static constexpr auto NOTE_ON = _data[static_cast<char>(MidiEvents::NOTE_ON)];
};

据此:

int main() {
    // Here's some pseudo code usage:
    constexpr 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:
    // Get the number of read bytes required for a MIDI event with a type equal to the value of nTestValue.
    constexpr char nBytesNeeded = MidiEventTypes::get(nTestValue).num_bytes; 
    // Or alternatively:
    constexpr char nBytesNeeded2 = MidiEventTypes::GetRequiredBytesByEventType(nTestValue);
    // and:
    constexpr char nBytesNeeded3 = MidiEventTypes::GetRequiredBytesByEventType(MidiEvents::NOTE_OFF);
}

it won't let me create a static unordered_map

是的,unordered_map 分配内存并对其中的内容进行排序。只使用普通数组,不要在任何地方分配内存——编译时就知道了。

这是我使用模板的结果。我使用的是 int 而不是 char,但您可以更改这些以满足您的需要。直播代码here

#include <iostream>

template <int V, int B>
struct MidiEventType
{
    static constexpr int value = V;

    static constexpr int bytes = B;

    constexpr operator int() const
    {
        return V;
    }
};

// dummy classes, used for accessing a given property from MidiEventType
// create as many as the number of properties in MidiEventType and specialize GetProperty for each
struct Value;
struct Bytes;

template <class T, class Property>
struct GetProperty;

template <class T>
struct GetProperty<T, Value>
{
    static constexpr auto property = T::value;
};

template <class T>
struct GetProperty<T, Bytes>
{
    static constexpr auto property = T::bytes;
};

struct MidiEventTypes
{
    static constexpr MidiEventType<8,2> NOTE_OFF{};
    static constexpr MidiEventType<9,2> NOTE_ON{};
    static constexpr MidiEventType<10,2> KEY_AFTERTOUCH{};
    static constexpr MidiEventType<11,2> CONTROL_CHANGE{};
    static constexpr MidiEventType<12,1> PROGRAM_CHANGE{};
    static constexpr MidiEventType<13,1> CHANNEL_AFTERTOUCH{};
    static constexpr MidiEventType<14,2> PITCH_WHEEL_CHANGE{};
    static constexpr MidiEventType<-1,-1> INVALID{};

    // perform the lookup
    template <class Property>
    static constexpr auto get(int key)
    {
        return get_impl<Property, decltype(NOTE_OFF), decltype(NOTE_ON),
                decltype (KEY_AFTERTOUCH), decltype (CONTROL_CHANGE),
                decltype (PROGRAM_CHANGE), decltype (CHANNEL_AFTERTOUCH),
                decltype (PITCH_WHEEL_CHANGE)>::call(key);
    }

private:

    // class to automate the construction of if/else branches when looking up the key
    // our template parameters here will be MidiEventType<X,Y>
    template <class Property, class T, class... Rest>
    struct get_impl
    {
        static constexpr auto call(int key)
        {
            if(T::value == key) return GetProperty<T, Property>::property;
            else return get_impl<Property, Rest...>::call(key);
        }
    };

    // specialization for a single class
    // if the key is not found then return whatever we've set for the INVALID type
    template <class Property, class T>
    struct get_impl<Property, T>
    {
        static constexpr auto call(int key)
        {
            if(T::value == key) return GetProperty<T, Property>::property;
            else return GetProperty<decltype(INVALID), Property>::property;
        }
    };
};

int main()
{
    std::cout << MidiEventTypes::CHANNEL_AFTERTOUCH.bytes << std::endl;
    std::cout << MidiEventTypes::get<Value>(MidiEventTypes::NOTE_OFF) << std::endl;
    std::cout << MidiEventTypes::get<Bytes>(MidiEventTypes::CHANNEL_AFTERTOUCH) << std::endl;
    std::cout << MidiEventTypes::get<Bytes>(42) << std::endl; // invalid key, return INVALID.bytes
}

除了我提出的使用模板的解决方案之外,这里还有一个基于 Jarod42 回答的更简单的解决方案。我们将使用数组并利用键是连续的 (8->14) 这一事实。查找密钥时,我们只需减去 8;这样数组就可以容纳 7 个元素。这种方法比模板更简单,但只能在要查找的值连续时使用。直播代码here

#include <iostream>
#include <array>

struct MidiEventType
{
    char value;
    char bytes;
    
    constexpr operator char() const { return value; }
};

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};
    
    static constexpr std::array<MidiEventType, 7> events{NOTE_OFF, NOTE_ON, KEY_AFTERTOUCH, CONTROL_CHANGE, PROGRAM_CHANGE, CHANNEL_AFTERTOUCH, PITCH_WHEEL_CHANGE};
    
    static constexpr auto get(char key)
    {
        // offset the key by 8 and then look into the array
        return events[(std::size_t)key - 8];
    }
};

int main()
{
    MidiEventTypes::get(MidiEventTypes::CONTROL_CHANGE).bytes;
    MidiEventTypes::get(MidiEventTypes::PROGRAM_CHANGE).bytes;
}

这是我的看法,一个完整的 constexpr 编译时解决方案。 为了您的使用,还将 midi 内容放入头文件中,您就可以开始了。

有头文件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;
}

// 接受后编辑:语法更清晰

#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;
}

我看到两个可行的解决方案:

  1. 如果您的数据高度不均匀并且层次结构可以更深,使用 JSON 将起作用并提供足够的灵活性,例如使用 Niels Lohmann's C++ json library。你会失去一些性能和类型安全性,但你在如何构建数据和数据类型方面非常灵活。

  2. 如果性能和类型安全更重要,您可以使用有限但性能更高的代码,例如:

#include <iostream>
#include <map>
#include <vector>

enum class Events { NOTE_OFF, PROGRAM_CHANGE };

const std::map<Events, const int> bytes_per_events = {
    {Events::NOTE_OFF, 2},
    {Events::PROGRAM_CHANGE, 1}
    // ...
};

int main()
{
    std::cout << bytes_per_events.at(Events::NOTE_OFF) << " "
              << bytes_per_events.at(Events::PROGRAM_CHANGE) << "\n";
    return 0;
}

您可以使用 类 代替整数或使用不同的容器,具体取决于需要什么。

这里肯定有很多很好的聪明解决方案,但我觉得有人需要提供代表简单方法的方法。只要您不需要一直使用方括号来查找元数据,就可以在 constexpr 函数中使用 switch 语句。这是我的解决方案:

#include <iostream>

namespace MidiEvents {

struct MidiEventMetaData {
    int num_bytes;
    const char *str;
    uint32_t stuff;
};

enum MidiEventTypes {
   NOTE_OFF = 8,
   NOTE_ON = 9,
   KEY_AFTERTOUCH = 10,
   CONTROL_CHANGE = 11,
   PROGRAM_CHANGE = 12,
   CHANNEL_AFTERTOUCH = 13,
   PITCH_WHEEL_CHANGE = 14,
   OTHER = 17
};

constexpr MidiEventMetaData get(char event_type)
{
    switch (event_type) {
    default:
        break;
    case NOTE_OFF:
        return { 1, "note off", 7 }; 
    case NOTE_ON:
        return { 1, "note on", 20 }; 
    case KEY_AFTERTOUCH:
        return { 2, "aftertouch", 100 };
    }
    return { 0, "unknown", 0 };
}

constexpr char GetRequiredBytesByEventType(char event_type)
{
    return get(event_type).num_bytes;
}

constexpr const char *GetEventNameByType(char event_type)
{
    return get(event_type).str;
}

} // namespace MidiEvents

int main(int argc, char **argv)
{
    char num_bytes = MidiEvents::GetRequiredBytesByEventType(MidiEvents::KEY_AFTERTOUCH);
    const char * const name = MidiEvents::GetEventNameByType(MidiEvents::KEY_AFTERTOUCH);
    std::cout << "name = " << name << "\n"; 
    std::cout << "num_bytes = " << (int)num_bytes << "\n";
    return 0;
}

需要注意的是,在实践中,编译器不会将所有这些分解为实际常量,直到您使用 -O2 进行构建。在 godbolt 上查看。你可以清楚地看到主函数只是调用 cout,传入常量值。如果删除 -O2,情况将不再如此。

这里的优点是这段代码非常接近您在最简单的场景中编写的代码。几乎每个人都可以理解,需要最少的非易失性存储,并且对事件值排序等没有限制。

解决方案:

首先我们制作一个类型的通用映射器,map_t。为此,我们要求每个类型(映射到)都有一个名为 key:

static constexpr
template <auto, auto, typename>
struct type_if_equal {};

template <auto k, typename T>
struct type_if_equal <k, k, T> : T {};

template <auto k, typename ... Ts>
struct map_t : type_if_equal<k, Ts::key, Ts>... {};

对于 OP 的问题,我们将数据放入 struct 及其关联的 Event 作为 key。最后我们用 using:

把它包装成用户友好的东西
struct Midi {

    enum class Event : char {
        NOTE_OFF = 8,
        NOTE_ON,    // +1 till specified
        KEY_AFTERTOUCH,
        CONTROL_CHANGE,
        PROGRAM_CHANGE,
        CHANNEL_AFTERTOUCH,
        PITCH_WHEEL_CHANGE
    };
    
private:
    // D = Data (shortened for re-use in mapping)
    template <Event e, int bytes /* other data */ >
    struct D {
        constexpr static Event key = e;
        constexpr static int BytesRequired = bytes;
        /* store other data here */
    };
    
public:
    
    template <Event e>
    using Info = map_t<e,
        D<Event::NOTE_OFF, 2>,
        D<Event::NOTE_ON, 2>,
        D<Event::KEY_AFTERTOUCH, 2>,
        D<Event::CONTROL_CHANGE, 2>,
        D<Event::PROGRAM_CHANGE, 1>,
        D<Event::CHANNEL_AFTERTOUCH, 1>,
        D<Event::PITCH_WHEEL_CHANGE, 2>>;
};

演示:

我们实际上得到了一个名为 Info 的类型“数组”,它接受任何 Event 类型并为我们提供适当的 Data 类型(使用 static我们关心的数据)。


解决方案的一般性:

这里的一些其他答案 much 对于给定的特定示例问题更好(更简单但仍然有效)。但是,OP 要求提供比示例问题更通用的内容。

我认为这里的想法是我们可能想使用元编程 (MP) 来推导一个 event 值,然后 要访问适当的数据,我们需要一些实际上将 event 作为变量的东西,而不仅仅是一个名称(我认为这是 OP 感兴趣的功能)。我们可以让我们的 MP 依赖于数据收集,但这有更多的耦合 - 如果我们不编写 MP 代码怎么办?

在这个答案中,我假设 Key 类型 cannot 被更改以使其很好地用于映射。我也不认为键会有很好的顺序来进行简单映射:对于 OP,我们可以映射 array[event - 8],但这不是通用解决方案。

这是针对小众问题的小众解决方案。请注意,我列出了两次 Event 元素——不是必须的——而是因为我在演示键定义和映射的分离。


解释:

直觉上数组似乎是最简单的选择,但我想避免生成索引映射。相反,我们使用编译器的本地映射。本来我的回答是这样的:

template <int b /* other data */ >
struct Data {
    constexpr static int BytesRequired = b;
    /* store other data here */
};

template <Event>
struct Info {};

// Specify mappings:
template <>
struct Info <Event::NOTE_OFF> : Data<2> {};

template <>
struct Info <Event::NOTE_ON> : Data<2> {};

template <>
struct Info <Event::KEY_AFTERTOUCH> : Data<2> {};

// ...

... 但是我想避免重复的样式,所以我使用了 pack expansion which effectively generates a list like above. I first saw this wonderful trick here 的“条件”多重继承。一开始可能看起来很奇怪,但它在元编程中很常见,而且它对编译器来说非常高效(比递归好得多),当然,没有 运行 时间开销。