存储小型、固定大小、分层的静态数据集的好方法是什么?
What's a good way to store a small, fixed size, hierarchical set of static data?
我正在寻找一种方法来存储小型 多维 数据集,这些数据在编译时已知并且永远不会更改。此结构的目的是充当存储在单个名称空间中的全局常量,但可以全局访问而无需实例化对象。
如果我们只需要一个级别的数据,有很多方法可以做到这一点。您可以将 enum
或 class
或 struct
与 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 的数据对.
这些是目标:
数据和大小在编译时已知,永远不会改变。
使用每个数据集的人类可读密钥访问一小组数据。关键应该
映射到特定的非线性整数。
每个数据集包含相同的成员数据集。 IE。每个 MidiEventType
都有一个 NumBytes
属性.
可以使用命名键或函数访问子项。
有了键(或表示键值的变量),我们应该
能够读取与该键的常量项关联的额外数据
指向,为额外数据使用另一个命名键。
我们不需要实例化一个class来读取这个数据,因为
没有任何变化,并且副本不应超过一份
数据集.
事实上,除了 include 指令外,访问数据不需要任何东西,因为它的行为应该像常量。
我们在运行时不需要这个对象。目标是通过使用命名标签结构存储数据组而不是在任何地方使用(模糊的)整数文字,使代码更有条理且更易于阅读。
这是一个常量,您可以深入研究...例如 JSON。
理想情况下,使用常量的值不需要转换。
我们应该避免重复数据并且可能不同步的冗余列表。例如,一旦我们定义了 NOTE_ON = 9
,文字 9
就不应出现在其他任何地方。应改用标签 NOTE_ON
,以便只能在一处更改值。
这是一个笼统的问题,MIDI只是作为一个例子。
常量应该可以有多个属性.
存储编译时已知的小型、固定大小、分层(多维)静态数据集的最佳方法是什么,具有与常量相同的用例?
怎么样:
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;
}
我看到两个可行的解决方案:
如果您的数据高度不均匀并且层次结构可以更深,使用 JSON 将起作用并提供足够的灵活性,例如使用 Niels Lohmann's C++ json library。你会失去一些性能和类型安全性,但你在如何构建数据和数据类型方面非常灵活。
如果性能和类型安全更重要,您可以使用有限但性能更高的代码,例如:
#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 的“条件”多重继承。一开始可能看起来很奇怪,但它在元编程中很常见,而且它对编译器来说非常高效(比递归好得多),当然,没有 运行 时间开销。
我正在寻找一种方法来存储小型 多维 数据集,这些数据在编译时已知并且永远不会更改。此结构的目的是充当存储在单个名称空间中的全局常量,但可以全局访问而无需实例化对象。
如果我们只需要一个级别的数据,有很多方法可以做到这一点。您可以将 enum
或 class
或 struct
与 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 的数据对.
这些是目标:
数据和大小在编译时已知,永远不会改变。
使用每个数据集的人类可读密钥访问一小组数据。关键应该 映射到特定的非线性整数。
每个数据集包含相同的成员数据集。 IE。每个
MidiEventType
都有一个NumBytes
属性.可以使用命名键或函数访问子项。
有了键(或表示键值的变量),我们应该 能够读取与该键的常量项关联的额外数据 指向,为额外数据使用另一个命名键。
我们不需要实例化一个class来读取这个数据,因为 没有任何变化,并且副本不应超过一份 数据集.
事实上,除了 include 指令外,访问数据不需要任何东西,因为它的行为应该像常量。
我们在运行时不需要这个对象。目标是通过使用命名标签结构存储数据组而不是在任何地方使用(模糊的)整数文字,使代码更有条理且更易于阅读。
这是一个常量,您可以深入研究...例如 JSON。
理想情况下,使用常量的值不需要转换。
我们应该避免重复数据并且可能不同步的冗余列表。例如,一旦我们定义了
NOTE_ON = 9
,文字9
就不应出现在其他任何地方。应改用标签NOTE_ON
,以便只能在一处更改值。这是一个笼统的问题,MIDI只是作为一个例子。
常量应该可以有多个属性.
存储编译时已知的小型、固定大小、分层(多维)静态数据集的最佳方法是什么,具有与常量相同的用例?
怎么样:
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;
}
我看到两个可行的解决方案:
如果您的数据高度不均匀并且层次结构可以更深,使用 JSON 将起作用并提供足够的灵活性,例如使用 Niels Lohmann's C++ json library。你会失去一些性能和类型安全性,但你在如何构建数据和数据类型方面非常灵活。
如果性能和类型安全更重要,您可以使用有限但性能更高的代码,例如:
#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 的“条件”多重继承。一开始可能看起来很奇怪,但它在元编程中很常见,而且它对编译器来说非常高效(比递归好得多),当然,没有 运行 时间开销。