There are two existing classes, one is SrcField
which returns the concrete type value, and the other is a union DSTField
, that defines the corresponding data type.
class SrcField
{
public:
signed char GetInt8();
unsigned char GetUInt8();
short GetInt16();
unsigned short GetUInt16();
int GetInt32();
unsigned int GetUInt32();
float GetFloat();
double GetDouble();
bool GetBool();
DataType GetType();
private:
DataType m_type;
DSTField m_data;
};
union DSTField
{
signed char m_int8;
unsigned char m_uint8;
short m_int16;
unsigned short m_uint16;
int m_int32;
unsigned int m_uint32;
float m_float;
double m_double;
bool m_bool;
};
When I use both classes, the application is as below.
It's very redundant; is there any good way to simplify it, such as templates, generic programming, etc?
int main()
{
SrcField sf;
DSTField df;
switch(sf.GetType())
{
case TYPE_INT8:
df.m_int8 = sf.GetInt8();
break;
case TYPE_UINT8:
df.m_uint8 = sf.GetUInt8();
break;
case TYPE_INT16:
df.m_int16 = sf.GetInt16();
break;
case TYPE_UINT16:
df.m_uint16 = sf.GetUInt16();
break;
case TYPE_INT32:
df.m_int32 = sf.GetInt32();
break;
case TYPE_UINT32:
df.m_uint32 = sf.GetUInt32();
break;
case TYPE_FLOAT:
df.m_float = sf.GetFloat();
break;
case TYPE_DOUBLE:
df.m_double = sf.GetDouble();
break;
case TYPE_BOOL:
df.m_bool = sf.GetBool();
break;
default:
break;
}
}
Using std::variant
your code would look like this:
#include <iostream>
#include <variant>
typedef std::variant<
signed char,
unsigned char,
short,
unsigned short,
int,
unsigned int,
float,
double,
bool
> SrcField, DSTField;
int main()
{
SrcField sf(97.0f);
DSTField df;
df = sf;
if(auto pval = std::get_if<float>(&df))
std::cout << "variant value: " << *pval << '\n';
else
std::cout << "failed to get value!" << '\n';
}
Note: Since it's c++17, for previous versions I recommend to use boost::variant
, boost::any
or a header-only implementation of Any
class (for example I use one based on this in my project)
You said that you cannot change SrcField
, therefore a good solution could be the use of a visitor. The redundant code is still there, but it is present only once. See this:
template<typename Visitor>
constexpr void
visitField(Visitor&& visitor, SrcField& field)
{
switch(field.GetType())
{
case TYPE_INT8:
visitor(field.GetInt8());
break;
case TYPE_UINT8:
visitor(field.GetUInt8());
break;
....
default:
throw std::runtime_error("invalid type");
}
In this way you are able to use the values in a simple way:
int main()
{
SrcField field;
visitField([](auto value) {
if constexpr(std::is_same<decltype(value), double>::value)
std::cout << "Hey, double here!\n";
else if constexpr(std::is_same<decltype(value), bool>::value)
std::cout << "True or false?\n";
else
std::cout << "Other types\n";
std::cout << value << '\n';
}, field);
}
In this case I used the if constexpr
capability from C++17. Another possibility use a lambda overload
You can find a more complete example here on godbolt
Note: As you can see, I did not use DSTField
at all. If you really need to use DSTField
, you can use a similar approach:
template<typename T>
constexpr void
setField(DSTField& dstField, T value)
{
static_assert(std::is_arithmetic<T>::value,
"value must be an arithmetic type");
if constexpr(std::is_same<T, signed char>::value)
dstField.m_int8 = value;
else if constexpr(std::is_same<T, unsigned char>::value)
dstField.m_uint8 = value;
...
}
which can be used with something like
DSTField dest;
setField(dest, 4.f);
Other note: I marked the visitField
function as constexpr, but I cannot be sure if you can use in that way. Indeed, if SrcField::GetType
can only be executed at runtime, visitField
will never be executed at compile time.
Other other note: I don't know if this could depend on your code or not, but you have to keep in mind that you cannot be sure that signed char
is a std::int8_t
(as for most of the other types, obviously). You should use fixed width integer types if you want to make your code work as expected on foreign architectures.