4
\$\begingroup\$

I have some enum classes for units, like Weight units (KG and LB). I am translating the code to Qt C++ from Java, and I'm wondering what the best way to do this is as enums cannot have constructors in C++ that same way they do in Java (as far as I know).

Here is the Java class:

public enum WeightUnit {
  LB(WEIGHT_UNIT_LB, 0.45359237), //"lb" 1lb = 0.45359237 kg.
  KG(WEIGHT_UNIT_KG, 1.0);        //"kg"

  private String displayName;
  private double scale;

  private WeightUnit(String displayNameKey, double scale) {
      this.displayName = getDataMessages().getString(displayNameKey);
      this.scale = scale;
  }

  public String getDisplayName() {
      return displayName;
  }

  public String toString() {
      return displayName;
  }

  public static WeightUnit valueByName(String unitName) {
      for (WeightUnit value : WeightUnit.values()) {
          if (value.getDisplayName().equals(unitName) ||
                  value.name().equals(unitName)) {
              return value;
          }
      }
      throw new IllegalArgumentException(unitName);
  }

  public static double convert(double value, WeightUnit from, WeightUnit to) {
      if (from == to) return value;
      return value * from.scale/to.scale;
  }
}

Here is my C++ translation. It is not as elegant or clean as the Java version.

class WeightUnit {
  public:
    enum Value
    {
        LB,
        KG
    };

    WeightUnit() = default;
    constexpr WeightUnit(Value weightUnit) : value(weightUnit) {}

    bool operator==(WeightUnit a) const { return value == a.value; }
    bool operator!=(WeightUnit a) const { return value != a.value; }

    static double convert(double value, WeightUnit fromUnit, WeightUnit toUnit){
        return value * val(fromUnit.value)/val(toUnit.value);
    }

    static WeightUnit fromStr(QString str){
        if (str.compare(toStr(Value::LB)) == 0)
            return WeightUnit::LB;
        else if (str.compare(toStr(Value::KG)) == 0)
            return WeightUnit::KG;
        else
            throw QException();
    }

    static QString toStr(WeightUnit unit){
        if (unit == WeightUnit::LB)
            return "lb";
        else if (unit == WeightUnit::KG)
            return "kg";
        else
            throw QException();
    }

private:
    Value value;

    static double val(Value value){
        switch(value){
        case KG:
            return 1.0;
        case LB:
            return 0.45359237; //1 lb = 0.45359237 kg
        }
        throw QException(); //if not found, throw exception
    }
};

Is there any better way to tie values to each enum value in C++? In Java it is easy, because you can specify a constructor that requires certain parameters so you can be sure each enum value has an appropriate String and double value. Is there a way to do this in Qt/C++?

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Would you give some use examples? In what context will you manipulate weight units? If they're used as tags for numeric values, or in a conversion mechanism, I believe there are better options at your disposal than enums. \$\endgroup\$
    – papagaga
    Mar 6, 2019 at 7:59
  • \$\begingroup\$ They definitely function as enums, ie are passed around, written to a file and read back, selected by users in Settings, etc. Perhaps I should not be combining the enum and conversion functions in C++ since it is not OO in the same way Java is. \$\endgroup\$
    – Django
    Mar 6, 2019 at 15:13

1 Answer 1

2
\$\begingroup\$

Even if Java and C++ display some similarities, particularly in the syntax, they are widely different languages and translating one into the other isn't automatic. Java is chiefly object-oriented, while C++ is more of a multi-paradigm language; one could even argue that it's drifting away from the object-oriented model towards a more functional approach designed to take advantage of compile-time customization and calculations.

So, it's best to take a step back and think again about your objectives and the means you have to achieve them when you transition from one to the other.

What you seem to need is a mapping between a unit, a textual representation and a floating precision proportion. Java allows for float based enums, which are thus a good candidate -even if the textual representation needs to be addressed somewhere else- but C++ doesn't. On the other hand, in the most recent version of the C++ standard, you can create and manipulate arrays at compile-time:

constexpr std::pair<std::string_view, float> weight_units[] {
    { "kg", 1. },
    { "lb", 0.45359237 },
    { "oz", 0.028349523125 },
};

constexpr auto weight_unit_value(std::string_view unit) {
    return std::find_if(std::begin(weight_units), std::end(weight_units), [unit](auto uv) {
        return uv.first == unit;
    })->second;
}

This code sadly compiles only with the latest version of clang, because gcc hasn't implemented the constexpr version of the standard algorithms yet. But writing it by hand would take 5 minutes. Or you can simply remove the constexpr qualifier.

This code has several advantages over the original Java version, the most important being that everything is defined once in one place. If you want to create a new unit in the original code, you need to modify the enum and create a new object with a custom name; here, you just extend the array.

Now, the array is also very handy if you want to parse a weight from a string, or convert a weight to a string:

// ex: float weight = parse_weight("1204.3 lb");
auto parse_weight(std::string_view weight) {
    auto coeff =  std::find_if(std::begin(weight_units), std::end(weight_units), [weight](auto uv) {
        return weight.ends_with(uv.first);
    })->second;
    return std::stof(weight.data()) * coeff;
}

auto to_string(float value, std::string_view unit_name) {
    auto result = std::to_string(value / weight_unit_value(unit_name));
    auto out = std::back_inserter(result);
    *out++ = ' ';
    std::copy(unit_name.begin(), unit_name.end(), out);
    return result;
}

Error handling could be added very easily (auto found = std::find_if(...); if (found == std::end(units)) throw unknown_unit("blabla");

Here's some code to toy with: https://wandbox.org/permlink/zj0hckfk6jR0vrXT

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for the example code, this seems much cleaner than what I was doing. \$\endgroup\$
    – Django
    Mar 6, 2019 at 15:14

Your Answer

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

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