Have been using std::variant
for the first time.
Accidentally stumbled on a pattern that seems resonable but wanted to get a second opinion.
I have a class that can contain value (its for another language simulation). To represent this in C++ I though the value can use a variant and thus I can retain type safety. But now I need to get the data out and using std::variant::index
works but is less readable in the code than I want. So I initially added a type object to the class that I kept in sync with the variant:
Think:
// Note: This is not the code for review.
// This is to give context.
class Object {/*Stuff*/}
enum class DataType = {Integer, Double, String, Bool, Object};
using DataValue = std::vaiant<int, double, std::string, bool, Object>;
class Value
{
DataType typeInfo;
DataValue value;
public:
// Removed constructors for clarity.
DataType type() const {return typeInfo;}
template<typename T>
T const& get() const {return std::get<T>(value);}
};
This way I to perform actions on the value I can use a switch:
void doAction(Value& v)
{
switch (v.type()) {
case DataType::Integer: doAction(v.get<int>());break;
case DataType::Double: doAction(v.get<double>());break;
case DataType::String: doAction(v.get<std::string>());break;
case DataType::Bool: doAction(v.get<bool>());break;
case DataType::Object: doAction(v.get<Object>());break;
}
}
So two things stand out to me.
- In the
get<X>()
theX
does not match theDataType::X
apart formObject
. - I have stored an extra type object (I am sure that variant is already got).
To the code to review:
I changed two things. I don't store the extra typeInfo object (I calculate it from value) and I add some extra type aliases to make the code cleaner and easier to read:
This is what my code looks like now:
namespace ThorsAnvil::Anvil::Ice
{
class Object {/*Stuff*/}
enum class DataType = {Integer, Double, String, Bool, Object};
using Integer = int;
using Double = double;
using String = std::string;
using Bool = std::bool;
using DataValue = std::vaiant<Integer, Double, String, Bool, Object>;
class Value
{
DataValue value;
public:
// Removed constructors for clarity.
DataType type() const {return static_cast<DataType>(value.index());}
template<typename T>
T const& get() const {return std::get<T>(value);}
Value toInteger(Value& v) const
{
switch (v.type()) {
case DataType::Integer: return Value{get<Integer>()};break;
case DataType::Double: return Value{static_cast<int>(get<Double>())};break;
case DataType::String: return Value{std::atoi(get<String>().c_str())};break;
case DataType::Bool: return Value{get<Bool>()? 1 : 0};break;
case DataType::Object: return Value{get<Object>().toNumber()};break;
}
}
};
}
1 Answer 1
Use std::visit()
What you want can be done using std::visit()
. For example, you can write:
using DataValue = std::variant<int, double, std::string, bool, Object>;
void doAction(DataValue& v) {
std::visit([](auto& v){ doAction(v); }, v);
}
This basically is equivalent to your doAction()
, but now it works directly on a std::variant
instead of on your wrapper class. You of course still need to provide the specializations of doAction()
for each of the types of the variant.
If you want to write a single, compact toInteger()
function, then you can use the struct overloaded
trick:
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
DataValue toInteger(const DataValue& v) {
return std::visit(overloaded{
[](const int& v) { return v; },
[](const double& v) { return static_cast<int>(v); },
[](const std::string& v) { return std::stoi(v); },
[](const bool& v) { return v ? 1 : 0; },
[](const Object& v) { return v.toNumber(); }
}, v);
}
-
\$\begingroup\$ Thanks. That's nice. Have not used
std::visit()
(or heard about it). \$\endgroup\$Loki Astari– Loki Astari2024年01月30日 19:43:08 +00:00Commented Jan 30, 2024 at 19:43