Search code examples
c++nestedswitch-statement

Refactor nested switch statement on union members using modern C++


How can I use modern C++ to get the same effect as

enum Value_type_id { i, f, d };
union Value { int i; float f; double d; };
struct Item { Value_type_id type; Value value; };

Item add(Item lhs, Item rhs)
{
  Item result;
  switch (lhs.type)
  {
    case i:
      switch (rhs.type)
      {
        case i:
          result.type = i;
          result.value.i = lhs.value.i + rhs.value.i;
          break;
        case f:
          result.type = f;
          result.value.f = lhs.value.i + rhs.value.f;
          break;
        case d:
          result.type = d;
          result.value.d = lhs.value.i + rhs.value.d;
          break;
      }
      break;
    case f:
      switch (rhs.type)
      {
        case i:
          result.type = f;
          result.value.f = lhs.value.f + rhs.value.i;
          break;
        case f:
          result.type = f;
          result.value.f = lhs.value.f + rhs.value.f;
          break;
        case d:
          result.type = d;
          result.value.d = lhs.value.f + rhs.value.d;
          break;
      }
      break;
    case d:
      switch (rhs.type)
      {
        case i:
          result.type = d;
          result.value.d = lhs.value.d + rhs.value.i;
          break;
        case f:
          result.type = d;
          result.value.d = lhs.value.d + rhs.value.f;
          break;
        case d:
          result.type = d;
          result.value.d = lhs.value.d + rhs.value.d;
          break;
      }
      break;
  }
  return result;
}

but without having to tediously code the nested switch statement with nearly identical cases for this and other functions of two instances of Item? Ideally I'd like to be able to do something like

Item add(Item lhs, Item rhs)
{
  return some_magic_here(std::plus{}, lhs, rhs);
}

but I haven't figured out how to implement some_magic_here. Basing Item on std::variant, and some_magic_here on std::visit, seemed appropriate at first glance, until I discovered that std::visit requires the return value to have the same type for all alternatives represented in the variant. I need return types that are the std::common_type of the two argument types, which isn't always the same.

Actually, my use case has 8 different data types, which leads to 64 different cases that are identical except for which members of the unions are accessed.

Addition 2023-11-11: My application (an implementation of an interpreted language) is standalone and large (170'000 lines) and uses the enum/union/struct throughout. I can (with a lot of work) replace them with something else, but only wanted to do that if there was clear benefit, such as getting rid of the many near-duplicate switch cases for the different supported data types. The accepted answer (by Miles Budnek) provides that.

Addition 2023-11-15: I now realize that I need a similar solution for vectors, too, but I don't know how to extend the current question's answer to that case, so I've asked a follow-up question here: Refactor nested switch statement on union members with vectors, using modern C++


Solution

  • You are correct that the visitor passed to std::visit has to return the same type for all cases, but there's no reason that type can't itself be a std::variant:

    using Item = std::variant<int, float, double>;
    
    Item add(Item lhs, Item rhs)
    {
        return std::visit(
            [](auto lhs, auto rhs) { return Item{lhs + rhs}; },
            lhs,
            rhs
        );
    }
    

    Example

    The return type of the visitor is always std::variant<int, float, double>, but that variant holds a different type depending on the type contained in lhs and rhs.