Java "Object" in C++ using std::variant


I’m going through this brilliant book, Crafting Interpreters, these days to learn more about how interpreters are built. My attempts so far have been ameturish, as I’ve never had a formal CS course, and this looked useful to upskill my toolkit. However, the book implements the interpreter in Java (at least the first part, which I am going through now) and I’m trying to follow along in C++ instead since I don’t have much experience in Java, neither an inclination to learn it. In one of the chapters, the author uses a Java “Object” class to hold the literal values that may appear in the script being parsed by the interpreter. Java docs say that:

Class Object is the root of the class hierarchy. Every class has Object as a superclass. All objects, including arrays, implement the methods of this class.

So, this threw a bit of a spanner in the works for me since C++ does not really have any equivalent class which all classes derive from. Going through the options available, I stumbled upon std::conditional and std::variant that could be of help. From cppreference:

std::conditional

Defined in header <type_traits>

template< bool B, class T, class F > struct conditional; (since C++11)

Provides member typedef type, which is defined as T if B is true at compile time, or as F if B is false.

And

std::variant

Defined in header <variant>

template <class… Types> class variant; (since C++17)

The class template std::variant represents a type-safe union. An instance of std::variant at any given time either holds a value of one of its alternative types, or in the case of error

std::conditional seems to be much more straight forward to use if you have only two types to choose from. But I needed more than 2 right now, and they could increase in future as well if I started supporting more types. Thus, I went with std::variant. You can think of this as a safer version of a union. So, we’ll see how to achieve my goal of emulating a Java Object-like class using std::variant and the visitor pattern. Another thing I needed to do was to convert the value into a string, irrespective of which type it was, so we will see how I did that as well.

The code is pretty straight forward. First we declare a new type alias called Object. This refers to an std::variant which can take std::nullptr_t, std::string, a double or a bool types.

using Object = std::variant<std::nullptr_t, std::string, double, bool>;

Next we create a helper function that encapsulates our cute little trick to convert any incoming object into a string in C++ with minimal code.

template <typename T>
static std::string VisitorHelper(T x) {
  std::stringstream temp;
  temp << x;
  return temp.str();
}

Now, we could create tons of boilerplate code using things like std::get/std::get_if/std::in_place_index etc to start using our Object. But we want to be smart and avoid all this. So, we use a visitor pattern. You can read this awesomely simple article by Bartlomiej Filipek to understand your options better.

We create a Visitor struct which contains the overloads for the operator(), which will call the appropriate implementation based on the type of the value of our instance. You can create other operator overloads as well here if you need. Note that in all the cases we are calling our templatized VisitorHelper which helps in converting the value to a string and return that.

struct Visitor {
  std::string operator()(std::nullptr_t x) {
    (void)x;  // Avoid unused parameter error
    return "null";
  };
  std::string operator()(double x) const { return VisitorHelper(x); }
  std::string operator()(bool x) const { return VisitorHelper(x); }
  std::string operator()(std::string x) const { return VisitorHelper(x); }
};

We can then “visit” the appropriate overload depending on the value with which it is called like so:

std::visit(Visitor{}, literal_);

Combining everything together, we get to something like below (Ignore the extra code that I have for the rest of my class), and we have our very own super class Object in C++.

namespace lox {

// TODO: Update the possible variants here as we go.
using Object = std::variant<std::nullptr_t, std::string, double, bool>;

class Token {
 public:
  Token() = delete;
  Token(const TokenType type, const std::string& lexeme, const Object& literal,
        const int line)
      : type_(type), lexeme_(lexeme), literal_(literal), line_(line) {}

  std::string ToString() {
    return "Line " + std::to_string(line_) + ": " +
           TokenTypeName[static_cast<size_t>(type_)] + " " + lexeme_ + " " +
           std::visit(Visitor{}, literal_);
  }

 private:
  const TokenType type_;
  const std::string lexeme_;
  const Object literal_;
  const int line_;

  template <typename T>
  static std::string VisitorHelper(T x) {
    std::stringstream temp;
    temp << x;
    return temp.str();
  }

  struct Visitor {
    std::string operator()(std::nullptr_t x) {
      (void)x;  // Avoid unused parameter error
      return "null";
    };
    std::string operator()(double x) const { return VisitorHelper(x); }
    std::string operator()(bool x) const { return VisitorHelper(x); }
    std::string operator()(std::string x) const { return VisitorHelper(x); }
  };
};

}  // namespace lox

Side note 1: Technically, it is not needed to create a struct for this and you can do it more concisely by creating a lambda within std::visit instead that calls VisitorHelper directly. I went the struct way because ultimately I want to be able to do diffferent actions depending on the type, so I need different implementations for them.

Side note 2: A friend pointed out that I could use std::any here as well. But I didn’t choose to use it because of the similar issues I mentioned earlier that it requires boilerplate for figuring out the type (e.g. in a switch case) and then getting the value after typecasting it appropriately.


See also