在运行时捕获的constexpr变量中捕获的内部数组会丢失
我正在尝试创建一个 [int/enum] 到文本的映射类,为其用户提供尽可能少的开销。应向其构造函数传递一个值到文本映射的列表,随后可以对其进行查询。创建的对象应该是 constexpr 并具有可选的大小参数,这允许编译器在编译时有选择地检查传递的映射数量是否与预期相符。当与枚举一起使用时,这作为额外的安全措施特别有用 - 也就是说:如果您忽略为新添加的枚举值添加映射,则可以强制编译错误。它应该在 Visual Studio 2019 和 Xcode 9 和 12 下与 C++14 一起使用。
我当前的尝试是下面的代码。但是,至少在 Visual Studio 2019 下,传递的映射数组未在 m_mappings
成员变量中正确捕获。当您运行此代码时,m_mappings
指向随机内存地址,因此您获得的任何输出都是错误的(如果它没有彻底崩溃)。
#include <iostream>
template <typename Type>
struct Mapping {
Type value;
const char* text;
};
template <typename Type, Type maxValue = Type(-1)>
class Mapper {
public:
template <size_t numMappings>
explicit constexpr Mapper(const Mapping<Type>(&mappings)[numMappings]) :
m_mappings(mappings),
m_numMappings(numMappings) {
static_assert(
int(maxValue) == -1 || numMappings == int(maxValue) + 1,
"Some mappings are missing!"
);
}
const char* Map(Type value) const {
for (size_t mappingNr = 0; mappingNr < m_numMappings; ++mappingNr)
if (m_mappings[mappingNr].value == value)
return m_mappings[mappingNr].text;
return "?";
}
private:
const Mapping<Type>* m_mappings;
const size_t m_numMappings;
};
enum class TestEnum {
a,
b,
c,
maxValue = c
};
int main() {
constexpr Mapper<int> intMapper_noCheck({
{ 11, "a" },
{ 5, "b" },
{ 26, "x" }
});
std::cout << intMapper_noCheck.Map(10);
constexpr Mapper<int, 3> intMapper_check({
{ 0, "z" },
{ 1, "f" },
{ 2, "t" },
{ 3, "#" }
});
std::cout << intMapper_check.Map(2);
// if we'd pass e.g. TestEnum::b here, we get a nice compile time error
constexpr Mapper<TestEnum, TestEnum::maxValue> enumMapper({
{ TestEnum::a, "-" },
{ TestEnum::b, "-" },
{ TestEnum::c, "+" }
});
std::cout << enumMapper.Map(TestEnum::b);
// expected output by now: "?t-"
std::cin.get();
return 0;
}
一种可能的解决方案是在单独的 constexpr 变量中捕获映射数组,并将其传递给映射器对象,如下所示:
constexpr Mapping<TestEnum> enumMapping[] = {
{ TestEnum::a, "-" },
{ TestEnum::b, "-" },
{ TestEnum::c, "+" }
};
constexpr Mapper<TestEnum, TestEnum::maxValue> enumMapper(enumMapping);
std::cout << enumMapper.Map(TestEnum::b);
这样映射就会得到保留并且输出是正确的。然而,我发现这个额外的层使它变得更加“混乱”......
这里的复杂因素是传入的数组的大小必须以 constexpr 友好的方式捕获,并且我不想通过单独指定它手。
- 使用构造函数模板参数中指定大小的固定大小数组是实现此目的的一种方法,但是当内联传递数组时,它在运行时无法访问。
- 另一个版本是传递(并存储)一个
std::initializer_list
,但您不能在它的size
方法上使用static_assert
。作为一种解决方法,我们还可以像m_mappings(sizeMatches?mappings: throw "mismatch!")
一样初始化m_mappings
,但这样非 constexpr 对象可能会在运行时执行令人讨厌的抛出(不幸的是,代码库的其余部分并不完全是异常安全的)。 - 我考虑使用
std::array
代替(其中的size
是 constexpr 可以访问的),但是没有办法将使用的大小传递给m_mappings (大小只有构造函数模板知道,类模板不知道)。
- 我还考虑为构造函数使用模板参数包,以便用户可以两两传递松散的值和文本参数。但是我该如何将它们填充到 m_mappings 中呢?例如,我必须将
m_mappings
设为std::vector
才能执行此操作,但这与 constexpr 不兼容。
像我的第一个版本一样,在保持美观和干净的同时,还有什么选择呢?
I'm trying to create an [int/enum]-to-text mapping class with as little overhead as possible for it's users. It's constructor should be passed a list of value-to-text mappings, which can be queried afterwards. Created objects should be constexpr and have an optional size argument, which allows the compiler to optionally check at compile time if the number of passed mappings matches what is expected. This is especially useful as an extra safety measure when used with enums -- that is: this way you can force a compile error if you neglect to add a mapping for newly added enum values. It should work with C++14 under Visual Studio 2019 and Xcode 9 and 12.
My current stab at this is the code below. However, under Visual Studio 2019 at least, the passed array of mappings is not correctly captured in the m_mappings
member variable. When you run this code, m_mappings
points to a random memory address, so any output you get is wrong (if it doesn't outright crash).
#include <iostream>
template <typename Type>
struct Mapping {
Type value;
const char* text;
};
template <typename Type, Type maxValue = Type(-1)>
class Mapper {
public:
template <size_t numMappings>
explicit constexpr Mapper(const Mapping<Type>(&mappings)[numMappings]) :
m_mappings(mappings),
m_numMappings(numMappings) {
static_assert(
int(maxValue) == -1 || numMappings == int(maxValue) + 1,
"Some mappings are missing!"
);
}
const char* Map(Type value) const {
for (size_t mappingNr = 0; mappingNr < m_numMappings; ++mappingNr)
if (m_mappings[mappingNr].value == value)
return m_mappings[mappingNr].text;
return "?";
}
private:
const Mapping<Type>* m_mappings;
const size_t m_numMappings;
};
enum class TestEnum {
a,
b,
c,
maxValue = c
};
int main() {
constexpr Mapper<int> intMapper_noCheck({
{ 11, "a" },
{ 5, "b" },
{ 26, "x" }
});
std::cout << intMapper_noCheck.Map(10);
constexpr Mapper<int, 3> intMapper_check({
{ 0, "z" },
{ 1, "f" },
{ 2, "t" },
{ 3, "#" }
});
std::cout << intMapper_check.Map(2);
// if we'd pass e.g. TestEnum::b here, we get a nice compile time error
constexpr Mapper<TestEnum, TestEnum::maxValue> enumMapper({
{ TestEnum::a, "-" },
{ TestEnum::b, "-" },
{ TestEnum::c, "+" }
});
std::cout << enumMapper.Map(TestEnum::b);
// expected output by now: "?t-"
std::cin.get();
return 0;
}
A possible solution is to capture the mapping array in a separate constexpr variable, and pass that to the mapper objects, like so:
constexpr Mapping<TestEnum> enumMapping[] = {
{ TestEnum::a, "-" },
{ TestEnum::b, "-" },
{ TestEnum::c, "+" }
};
constexpr Mapper<TestEnum, TestEnum::maxValue> enumMapper(enumMapping);
std::cout << enumMapper.Map(TestEnum::b);
This way the mappings do get preserved and the output is correct. However, I find this extra layer makes it much more 'messy'...
The complicating factor here is that the passed in array's size must be captured in a constexpr-friendly way, and I do not want to have to specify it separately by hand.
- Using a fixed size array with the size specified in the constructor's template argument is one way of doing it, but when passing an array in-line it is thus not accessible at runtime.
- Another version would be to pass (and store) a
std::initializer_list
, but you can'tstatic_assert
on it'ssize
method. As a workaround we could also initializem_mappings
likem_mappings(sizeMatches? mappings: throw "mismatch!")
, but that way non-constexpr objects may perform nasty throws at runtime (and unfortunately the rest of the code base isn't exactly exception safe). - I contemplated using a
std::array
instead (that one'ssize
is constexpr accessible), but then there is no way to pass the used size tom_mappings
(the size is only known to the constructor template, and not to the class template). - I also contemplated using a template parameter pack for the constructor so users can pass the loose value-and-text args two-by-two. But then how do I stuff these into
m_mappings
? I'd have to e.g. makem_mappings
astd::vector
to do so, but that's not constexpr-compatible.
What could be another option whilst keeping it nice and clean like in my first version?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
失败的原因是,您将一个指针存储到将数组传递给构造函数以供以后使用的指针。
但是在您的紧凑型情况下,该数组在构造函数运行后不再存在。因此,您需要进行一些分配(在所有编译器中可能不友好)并复制。您使用
char*
而不是std :: string
进一步添加到此问题的事实 - 也不能保证当指向字符串时仍存在您使用它。另外,除非这是针对某种课程的工作“ rel =“ nofollow noreferrer”> https://github.com/serge-sans-paille/frozen
The reason it fails is that you store a pointer to the array passed to the constructor for later use.
But in your compact cases, that array no longer exists after the constructor has run. So you need to do some allocation (which might not be constexpr-friendly in all compilers) and copying. The fact that you use a
char*
instead of astd::string
further adds to this problem - there too there is no guarantee that the pointed-to string still exists when you use it.Also, unless this is for some sort of coursework, consider looking at the
constexpr
implementations of sets and maps provided by https://github.com/serge-sans-paille/frozen经过一番篡改之后,我提出了一个解决方案,在该解决方案中,我只是将经过的映射复制到一个普通的旧数组成员中。我还尝试使用std ::数组,但是在C ++ 14中它不够友好。
我先前尝试过的是自动捕获模板构造函数中的映射列表大小(
nummappings
是由编译器推导的),然后将其匹配到类模板中指定的预期映射数(MaxValue )。但是现在,类模板本身需要知道我们将要通过的映射数量,以便可以为该副本保留存储。因此,我还将
maxValue
参数重新定义以代表这一点。因此,一个缺点是您现在始终需要手动计算要通过多少次映射,这在绘制大型不连续的INT范围内是您真正不关心该细节的痛苦。对于枚举而言,什么都没有改变,我主要写这堂课来处理枚举。
因此,这不是一个100%完美的问题,但是我想……好吧。
After some more tampering I came up with a solution where I just copy over the passed mappings into a plain old array member. I also tried to use an std::array, but it just isn't constexpr-friendly enough in C++14.
What I tried previously was to automatically capture the mapping list size in the templated constructor (
numMappings
is deduced by the compiler), and then match it to the specified expected number of mappings from the class template (maxValue
). But now the class template itself needs to know the number of mappings we're going to pass, so that it can reserve storage for the copy. So I've also repurposed themaxValue
parameter to represent exactly that.A drawback is thus that you now always need to count out manually how many mappings you're going to pass, which is a pain when mapping over large discontinuous int ranges where you'd really like not to care about that detail. For enums nothing really changes though, and I mainly wrote this class for handling enums.
So it's not a 100% perfect fit with the question, but it'll do I suppose... ah well.