constexpr 变量中捕获的内联数组在运行时丢失
Captured in-line array in constexpr variable gets lost on runtime
我正在尝试创建一个 [int/enum] 到文本的映射 class,并尽可能减少用户的开销。它的构造函数应该传递一个值到文本映射的列表,之后可以查询。创建的对象应该是 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);
这样映射确实得到保留并且输出是正确的。但是,我发现这个额外的层使它变得更加 'messy'...
这里的复杂因素是传入的数组大小必须以 constexpr 友好的方式捕获,我不想手动单独指定它。
- 使用在构造函数的模板参数中指定大小的固定大小数组是一种方法,但是当以内联方式传递数组时,它在 运行 时间无法访问。
- 另一个版本是传递(和存储)
std::initializer_list
,但您不能 static_assert
它的 size
方法。作为一种解决方法,我们也可以像 m_mappings(sizeMatches? mappings: throw "mismatch!")
一样初始化 m_mappings
,但那样的话,非 constexpr 对象可能会在 运行 时执行讨厌的抛出(不幸的是,代码库的其余部分并不完全异常安全)。
- 我打算改用
std::array
(那个 size
是 constexpr 可访问的),但是没有办法将使用的大小传递给 m_mappings
(大小仅为构造函数模板所知,而不为 class 模板所知)。
- 我还考虑过为构造函数使用模板参数包,以便用户可以两两传递松散的值和文本参数。但是,我该如何将它们填充到
m_mappings
中呢?我不得不例如使 m_mappings
成为 std::vector
来这样做,但这与 constexpr 不兼容。
还有什么选择可以像我的第一个版本一样保持美观和干净?
它失败的原因是您存储了一个指向传递给构造函数的数组的指针供以后使用。
但在您的紧凑型案例中,在构造函数具有 运行 之后该数组不再存在。所以你需要做一些分配(在所有编译器中可能不是 constexpr-friendly)和复制。您使用 char*
而不是 std::string
的事实进一步加剧了这个问题 - 也不能保证 pointed-to 字符串在您使用时仍然存在。
此外,除非这是为了某种课程作业,否则请考虑查看 https://github.com/serge-sans-paille/frozen
提供的集合和映射的 constexpr
实现
经过更多的篡改后,我想出了一个解决方案,我只是将传递的映射复制到一个普通的旧数组成员中。我也尝试使用 std::array,但它在 C++14 中 constexpr-friendly 还不够。
我之前尝试的是在模板化构造函数中自动捕获映射列表大小(numMappings
由编译器推断),然后将其匹配到指定的期望映射数从class 模板 (maxValue
)。但是现在 class 模板本身需要知道我们要传递的映射数量,以便它可以为副本保留存储空间。因此,我还重新调整了 maxValue
参数的用途以准确表示这一点。
因此,一个缺点是您现在总是需要手动计算要传递的映射数量,这在映射到您真正不想关心的大的不连续 int 范围时会很痛苦细节。对于枚举,虽然没有什么真正的改变,但我写这个 class 主要是为了处理枚举。
所以这不是 100% 完美地符合问题,但我想它会……啊好吧。
#include <iostream>
template <typename Type>
struct Mapping {
Type value;
const char* text;
};
template <typename Type, Type maxValue>
class Mapper {
public:
template <size_t numMappings>
explicit constexpr Mapper(const Mapping<Type>(&mappings)[numMappings]) :
m_mappings{} {
constexpr int correctNumMappings{ int(maxValue) + 1 };
static_assert(numMappings <= correctNumMappings, "Too many mappings given!");
static_assert(numMappings >= correctNumMappings, "Some mappings are missing!");
for (size_t mappingNr = 0; mappingNr < numMappings; ++mappingNr)
m_mappings[mappingNr] = mappings[mappingNr];
}
const char* Map(Type value) const {
for (const Mapping<Type>& mapping : m_mappings)
if (mapping.value == value)
return mapping.text;
return "?";
}
private:
Mapping<Type> m_mappings[int(maxValue) + 1];
};
enum class TestEnum {
a,
b,
c,
maxValue = c
};
int main() {
constexpr Mapper<int, 3> intMapper_check({
{ 0, "z" },
{ 1, "f" },
{ 2, "t" },
{ 3, "#" }
});
std::cout << intMapper_check.Map(2) << "\n";
constexpr Mapper<TestEnum, TestEnum::maxValue> enumMapper({
{ TestEnum::a, "-" },
{ TestEnum::b, "-" },
{ TestEnum::c, "+" }
});
std::cout << enumMapper.Map(TestEnum::b) << "\n";
std::cin.get();
return 0;
}
我正在尝试创建一个 [int/enum] 到文本的映射 class,并尽可能减少用户的开销。它的构造函数应该传递一个值到文本映射的列表,之后可以查询。创建的对象应该是 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);
这样映射确实得到保留并且输出是正确的。但是,我发现这个额外的层使它变得更加 'messy'...
这里的复杂因素是传入的数组大小必须以 constexpr 友好的方式捕获,我不想手动单独指定它。
- 使用在构造函数的模板参数中指定大小的固定大小数组是一种方法,但是当以内联方式传递数组时,它在 运行 时间无法访问。
- 另一个版本是传递(和存储)
std::initializer_list
,但您不能static_assert
它的size
方法。作为一种解决方法,我们也可以像m_mappings(sizeMatches? mappings: throw "mismatch!")
一样初始化m_mappings
,但那样的话,非 constexpr 对象可能会在 运行 时执行讨厌的抛出(不幸的是,代码库的其余部分并不完全异常安全)。 - 我打算改用
std::array
(那个size
是 constexpr 可访问的),但是没有办法将使用的大小传递给m_mappings
(大小仅为构造函数模板所知,而不为 class 模板所知)。 - 我还考虑过为构造函数使用模板参数包,以便用户可以两两传递松散的值和文本参数。但是,我该如何将它们填充到
m_mappings
中呢?我不得不例如使m_mappings
成为std::vector
来这样做,但这与 constexpr 不兼容。
还有什么选择可以像我的第一个版本一样保持美观和干净?
它失败的原因是您存储了一个指向传递给构造函数的数组的指针供以后使用。
但在您的紧凑型案例中,在构造函数具有 运行 之后该数组不再存在。所以你需要做一些分配(在所有编译器中可能不是 constexpr-friendly)和复制。您使用 char*
而不是 std::string
的事实进一步加剧了这个问题 - 也不能保证 pointed-to 字符串在您使用时仍然存在。
此外,除非这是为了某种课程作业,否则请考虑查看 https://github.com/serge-sans-paille/frozen
提供的集合和映射的constexpr
实现
经过更多的篡改后,我想出了一个解决方案,我只是将传递的映射复制到一个普通的旧数组成员中。我也尝试使用 std::array,但它在 C++14 中 constexpr-friendly 还不够。
我之前尝试的是在模板化构造函数中自动捕获映射列表大小(numMappings
由编译器推断),然后将其匹配到指定的期望映射数从class 模板 (maxValue
)。但是现在 class 模板本身需要知道我们要传递的映射数量,以便它可以为副本保留存储空间。因此,我还重新调整了 maxValue
参数的用途以准确表示这一点。
因此,一个缺点是您现在总是需要手动计算要传递的映射数量,这在映射到您真正不想关心的大的不连续 int 范围时会很痛苦细节。对于枚举,虽然没有什么真正的改变,但我写这个 class 主要是为了处理枚举。
所以这不是 100% 完美地符合问题,但我想它会……啊好吧。
#include <iostream>
template <typename Type>
struct Mapping {
Type value;
const char* text;
};
template <typename Type, Type maxValue>
class Mapper {
public:
template <size_t numMappings>
explicit constexpr Mapper(const Mapping<Type>(&mappings)[numMappings]) :
m_mappings{} {
constexpr int correctNumMappings{ int(maxValue) + 1 };
static_assert(numMappings <= correctNumMappings, "Too many mappings given!");
static_assert(numMappings >= correctNumMappings, "Some mappings are missing!");
for (size_t mappingNr = 0; mappingNr < numMappings; ++mappingNr)
m_mappings[mappingNr] = mappings[mappingNr];
}
const char* Map(Type value) const {
for (const Mapping<Type>& mapping : m_mappings)
if (mapping.value == value)
return mapping.text;
return "?";
}
private:
Mapping<Type> m_mappings[int(maxValue) + 1];
};
enum class TestEnum {
a,
b,
c,
maxValue = c
};
int main() {
constexpr Mapper<int, 3> intMapper_check({
{ 0, "z" },
{ 1, "f" },
{ 2, "t" },
{ 3, "#" }
});
std::cout << intMapper_check.Map(2) << "\n";
constexpr Mapper<TestEnum, TestEnum::maxValue> enumMapper({
{ TestEnum::a, "-" },
{ TestEnum::b, "-" },
{ TestEnum::c, "+" }
});
std::cout << enumMapper.Map(TestEnum::b) << "\n";
std::cin.get();
return 0;
}