2
\$\begingroup\$

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.

  1. In the get<X>() the X does not match the DataType::X apart form Object.
  2. 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;
            }
        }

};

}
\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

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);
}
\$\endgroup\$
1
  • \$\begingroup\$ Thanks. That's nice. Have not used std::visit() (or heard about it). \$\endgroup\$ Jan 30 at 19:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.