如何存储一个小的、固定大小的、分层的静态数据集?

16

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

如果我们只需要一级数据,有许多方法可以做到这一点。您可以使用带有静态/常量变量的枚举结构体

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

我们可以使用这个类及其成员变量在程序的任何地方轻松比较数值变量:

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

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

以下是一些伪代码用法:

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

这个问题不是关于如何让已实例化的类做到这一点。我已经可以做到了。这个问题是关于如何存储和访问与常量相关/附加的“额外”常量(不变数据)。 (这个结构在运行时不是必需的!)或者如何创建多维常量。似乎可以使用静态类来完成这个任务,但是我已经尝试过以下代码的几个变体,每次编译器都会找到不同的问题:

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()无法找到map。如果我将GetBytesRequired()设为非静态,则可以找到该映射,但是我不能在没有MidiEventTypes实例的情况下调用它,而且我也不想要它的实例。

再次强调,这个问题不是关于如何修复编译错误的,而是关于存储静态/常量数据的适当结构和设计模式,其中数据不仅限于键/值对。

以下是目标:

  • 数据和大小在编译时已知,并且永远不会更改。

  • 使用人类可读的键访问一小组数据,每个集合都应映射到一个特定的、非线性整数。

  • 每个数据集包含相同的成员数据集。即每个MidiEventType都有一个NumBytes属性。

  • 可以使用命名键或函数访问子项。

  • 使用键(或代表键值的变量),我们应该能够读取与键指向的常量项相关联的额外数据,使用另一个命名键来读取。

  • 我们不需要实例化类来读取此数据,因为没有任何更改,并且不应有多个数据集的副本。

  • 实际上,除了include指令之外,不需要其他内容来访问数据,因为它应该像常量一样行为。

  • 我们不需要在运行时使用此对象。目标是通过存储具有命名标签结构的数据组,而不是在各处使用(模糊的)整数文字,使代码更有组织性和易于阅读。

  • 它是一个可以深入挖掘的常量...就像JSON一样。

  • 理想情况下,不需要进行强制转换即可使用常量的值。

  • 我们应该避免重复列表,以避免重复数据且可能不同步。例如,一旦我们定义了NOTE_ON = 9,则文字9不应出现在任何其他地方。应使用标签NOTE_ON,以便只需在一个地方更改该值。

  • 这是一个通用问题,MIDI只是一个示例。

  • 常量应该能够具有多个属性。

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


对我来说可以工作,但需要进行一些修改:https://ideone.com/Evm7Dg 除非你真的在节省几个字节,否则我会将字节值设为int。 - Retired Ninja
2
有一个维度,许多属性,而不是许多维度。 - KamilCuk
典型的应该是一个普通数组,我认为。 - user253751
@Nick,你看过你所做的东西的库版本吗?比如像https://github.com/serge-sans-paille/frozen这样的东西? - Florian Humblot
1
你说这个结构在运行时不是必需的,但是展示了一个例子代码,在其中从非constexpr键中查找属性。你需要你所描绘的运行时查找吗?还是你实际上总是使用字面常量键(例如NOTE_ON)? - Useless
显示剩余5条评论
8个回答

6

以下是我的看法,一种完整的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;
}

const_map 的第 72 行有点小技巧。它能够工作是因为键值对的相等运算符只检查键部分。这略微违反了我自己关于“不使用技巧”的规则。 - Pepijn Kramer
如果你还没有看过的话,你可能想看一下这个链接:https://github.com/lefticus/constexpr_all_the_things 以及相关的演示视频:https://www.youtube.com/watch?v=PJwd4JLYJJY。我在寻找答案时偶然发现了它,但我还没有仔细研究过代码。这是一个尝试在C++中创建`constexpr` JSON(风格)树和解析器的项目。 - Nick
有没有一种更分层的方式来访问数据,其中“bytes_required”是事件类型的属性,而不是需要事件类型的函数?如果可以避免复杂性和开销,理想的语法应该是这样的:MIDI::EVENTS[NOTE_ON]; // 9MIDI::EVENTS[NOTE_ON]::BYTES_REQUIRED; // 2 最后 MIDI::EVENTS[9]::BYTES_REQUIRED; 用于在解析时使用派生的整数进行查找。我认为,如果namespace control是一个类,可能有一种方法使bytesname字符串作为成员可访问。 - Nick
::BYTES_REQUIRED 语法需要静态变量,这将使评估移动到运行时。因此我不认为是这样的。目前这是我能想到的最清洁的解决方案。 - Pepijn Kramer
1
@Nick 哎呀,我看到我没有添加正确的链接来更新语法 **static_assert(midi(MIDI_NOTE_OFF).required_bytes == 2, "test failed");**,链接是 https://onlinegdb.com/lGp7zMNB6 - Pepijn Kramer
显示剩余3条评论

3

可以尝试以下内容:

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

我认为这需要一些类型的运算符重载,以便MidiEventTypes :: NOTE_OFF返回其MidiEventTypevalue属性。否则,您会得到类似于“无法将'MidiEventTypes :: NOTE_OFF'从'const MidiEventType'转换为'int'”的内容,因为它不知道在比较运算符中使用value - Nick
1
@Nick:我本来会使用NOTE_OFF.value,但是consexpr operator char() const { return value; }应该是一个替代方案,可以达到相同的语法效果。 - Jarod42
谢谢!这样可以比较值。但是当反过来时,如何使用表示值的int变量访问成员属性byteRequired?如果我们有int nEventType = 8;,那么我们需要使用该EventType值来获取数据,例如:int BytesRqd = MidiEventTypes :: [nEventType] .nBytesRequired; - Nick
NOTE_OFF.byteRequired?如果你想进行一些查找/寻找,你也可以使用 static constexpr std::array<MidiEventType , 7> {NOTE_OFF, NOTE_ON, KEY_AFTERTOUCH, ..} - Jarod42
这对我来说似乎是最好的解决方案。我们甚至可以让它更加简洁 像这样。我们可以很容易地添加一些其他方式来访问 value,但我不明白为什么我们要这样做。 - Elliott

3

以下是我使用模板的意见。我使用 int 而不是 char ,但您可以根据需要更改它们。实时代码在此处

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

这似乎是一个合适的解决方案,尽管我会将“get”函数和成员放在宏中,以确保您不会意外地漏掉其中任何一个。无论如何都+1。 - Florian Humblot

1

这里确实有很多好的巧妙解决方案,但我觉得还需要有人提供代表简单方法的选项。只要你不需要始终使用方括号来查找元数据,就可以在 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,那么情况将不再如此。
优点在于,这段代码非常接近于您在最简单的场景中编写的代码。几乎所有人都可以理解它,需要的非易失性存储绝对最少,并且没有对事件值排序等限制。

发布简单答案加1。我喜欢它使用标准库。正如我在另一个评论中所说,我感觉很快标准库中将会有某种“constexpr JSON”对象,因为似乎有兴趣开发这样的东西,并且已经有一些相关提案。 - Nick

1

解决方案:

首先,我们创建一个通用的类型映射器map_t。我们通过要求每个类型(映射到的类型)都有一个名为keystatic 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的问题,我们将数据与其相关的事件一起放入一个结构体中作为键。最后,我们使用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数据)。


解决方案的普适性:

这里的其他答案中有一些比给定的特定问题更好(更简单但仍然有效)。然而,OP要求的是比示例问题更通用的东西。

我认为这里的想法是,我们可能希望使用元编程(MP)来推断出一个event值,然后访问适当的数据,我们需要实际将event作为变量而不仅仅是名称进行访问(我认为这是OP感兴趣的功能)。我们可以使我们的MP依赖于数据收集,但这会更加紧密 - 如果我们没有编写MP代码怎么办?

在这个答案中,我假设Key类型不能被更改以使其适用于映射。我也不假设键将具有良好的排序以进行简单的映射:对于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,它有效地生成了像上面那样的列表。我第一次看到这个奇妙的技巧是在这里。起初可能看起来很奇怪,但在元编程中很常见,并且对于编译器来说效率非常高(比递归好得多),当然也没有运行时开销。


这里的想法是,我们可能想使用元编程来推断事件值,然后访问适当的数据,我们需要实际将事件作为变量而不仅仅是名称传入的东西... 这是正确的。在这里有很多好的信息。我觉得在未来的C++版本中,我要做的事情将是微不足道的,因为似乎(从观看CPPcon谈话如“constexpr all the things”)有兴趣在静态数据结构,但STD库需要一些调整和开发使其全部工作。 - Nick

0

只需编写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);
}

它不让我创建静态 unordered_map。
是的,unordered_map会分配内存并在其中排序。只需使用普通数组,在任何地方都不需要分配内存——所有内容都在编译时已知。

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


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

-1
除了我提出的使用模板的解决方案之外,这里还有一个基于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;
}

我曾经考虑过使用-8这种方法,但出于两个原因而避免使用:首先它看起来像一个“魔法数字”,可能会让其他程序员(比如我,半年后)感到有点困惑;其次,它只适用于特定情况,而我正在尝试寻找一种更通用的解决方案,可以在任何需要这种结构的时候应用(即不仅仅适用于MIDI事件)。我认为你的模板答案更好,所以我投了它一票。 :) - Nick

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接