Reputation: 357
I'm trying to write a templated function that will allow me to pass in an explicitly sized enum and it will call the correct serialize function based on the enum's size in C++ 11. This is what I currently have, which works fine:
template<typename EnumName>
uint32_t WriteBuffer::Write_enum(EnumName aValue)
{
constexpr uint32_t cNumBits = sizeof(EnumName) * 8;
switch(cNumBits)
{
case 8:
{
return Write_uint8(static_cast<uint8_t>(aValue));
}
break;
case 16:
{
return Write_uint16(static_cast<uint16_t>(aValue));
}
break;
case 64:
{
return Write_uint64(static_cast<uint64_t>(aValue));
}
break;
case 32:
default:
{
return Write_uint32(static_cast<uint32_t>(aValue));
}
break;
}
}
enum MyEnum : uint8_t
{
A,
B
}
void SomeFunction()
{
m_writeBuffer.Write_enum(m_myEnum);
}
However, this still requires some runtime work to run the switch. I'm curious if templates can be used to determine the correct thing to call at compile time, since the size of the enum is known at compile time. Something like this, though this obviously doesn't work:
template<typename EnumName>
uint32_t WriteBuffer::Write_enum(EnumName aValue)
{
constexpr uint32_t cNumBits = sizeof(EnumName) * 8;
Write_sizedenum<EnumName, cNumBits>(aValue);
}
template<typename EnumName, 8>
uint32_t WriteBuffer::Write_sizedenum(EnumName aValue)
{
return Write_uint8(static_cast<uint8_t>(aValue));
}
template<typename EnumName, 16>
uint32_t WriteBuffer::Write_sizedenum(EnumName aValue)
{
return Write_uint16(static_cast<uint16_t>(aValue));
}
// etc
Please note that, for code generation reasons, I will not know the size of the enum where the code calling Write_enum gets written, so I need some combination of overloads and templates that lets me call one function with one name that will work for all enum sizes.
Is this a possible thing to do with templates? Or is the runtime switch the best I can do?
Upvotes: 1
Views: 151
Reputation: 39648
You can simply create a chain of if constexpr
-else
statements in order to create something equivalent to your switch
but with the guarantee of compile-time decisions.
template<typename EnumName>
uint32_t WriteBuffer::Write_enum(EnumName aValue)
{
constexpr uint32_t bits = sizeof(EnumName) * 8;
if constexpr (bits == 8) {
return Write_uint8(static_cast<uint8_t>(aValue));
}
else if constexpr (bits == 16) {
return Write_uint16(static_cast<uint16_t>(aValue));
}
else if constexpr (bits == 32) {
return Write_uint32(static_cast<uint32_t>(aValue));
}
else if constexpr (bits == 64) {
return Write_uint64(static_cast<uint64_t>(aValue));
}
else {
// This condition is always false, but if we used
// static_assert(false), it would always fail to compile.
// We must create a dependency on the template parameter.
static_assert(!bits, "Error, unexpected size!");
}
}
enum MyEnum : uint8_t
{
A,
B
};
void SomeFunction()
{
m_writeBuffer.Write_enum(m_myEnum);
}
Personally, I wouldn't even bother with this kind of overloading and just add a single function that writes raw bytes, then convert any other type to bytes.
uint32_t WriteBuffer::Write_bytes(std::byte arr[], std::size_t size) {
// TODO: implement
}
template<typename EnumName>
uint32_t WriteBuffer::Write_enum(EnumName aValue)
{
std::byte buffer[sizeof(EnumName)];
std::memcpy(buffer, &aValue, sizeof(EnumName));
return Write_bytes(buffer, sizeof(EnumName));
}
It can be so much easier. If we are concerned with handling endianness, we can perform a conditional byte reversal of our type, if there is a mismatch.
template<std::endian Endian, typename T>
T fix_endian(T value)
{
if constexpr (Endian == std::endian::native) {
return value;
}
else {
auto integer = __builtin_bit_cast(/* equally sized uint type */, value);
// TODO: wrap builtins in portable wrapper
// bswap is same for GCC and clang
integer = __builtin_bswap(integer);
return __builtin_bit_cast(T, integer);
}
}
This endianness-handling is arguably not so easy anymore, but it's universally applicable to all types, including floating point types and enumerations.
Upvotes: 2
Reputation: 4079
For this very specialized case, you can use std::underlying_type
to extract the type name, and then rely on plain old overloading:
struct WriteBuffer
{
template<typename EnumName>
uint32_t Write_enum(EnumName aValue) {
Write_overloaded(static_cast<typename std::make_unsigned<typename std::underlying_type<EnumName>::type>::type>(aValue));
}
uint32_t Write_overloaded(uint8_t);
uint32_t Write_overloaded(uint16_t);
uint32_t Write_overloaded(uint32_t);
uint32_t Write_overloaded(uint64_t);
};
More generally, you can use tag dispatching, using std::integral_constant
to do the dispatch. Most compilers will happily elide the struct instance and reduce it to a simple function call:
struct WriteBuffer
{
template<typename EnumName>
uint32_t Write_enum(EnumName aValue) {
Write_enum(aValue, std::integral_constant<std::size_t, (sizeof(EnumName) * 8)> {});
}
template<typename EnumName>
uint32_t Write_enum(EnumName aValue, std::integral_constant<std::size_t, 8>);
template<typename EnumName>
uint32_t Write_enum(EnumName aValue, std::integral_constant<std::size_t, 16>);
template<typename EnumName>
uint32_t Write_enum(EnumName aValue, std::integral_constant<std::size_t, 32>);
template<typename EnumName>
uint32_t Write_enum(EnumName aValue, std::integral_constant<std::size_t, 64>);
};
Upvotes: 3