Search code examples
c++polymorphismoperator-overloadingvirtual-functionsfriend-function

doing operator overloading and polymorphism correctly


I have two wrapper classes for string and int and one to represent Binary Operation and overloading the operator << to write them in a string format. Unfortunately, I can't overload << without friend function and I can't get polymorphism without virtual and can't use friend with virtual !!

#include <iostream>
#include <string>

class AST {};
class expr: public AST {};

class operator_: public AST {};

class BinOp: public expr {
private:
  expr *_left;
  expr *_right;
  operator_ *_op;
public:
  BinOp(expr *p_left, expr *p_right, operator_ *p_op):_left(p_left),_right(p_right),_op(p_op) {}
  friend std::ostream &operator<< (std::ostream &out, const BinOp &binop) {
    out << (binop._left) << " " << (binop._op) << " " << (binop._right);
  }
};

class LShift: public operator_ {
public:
  LShift() {}
  friend std::ostream &operator<< (std::ostream &out, const LShift &lshift) {
    out << "<<";
  }
};

class Name: public expr {
private:
  std::string _id;
public:
  Name(std::string p_id) { _id = p_id; }
  friend std::ostream &operator<< (std::ostream &out, const Name &name) {
    out << name._id;
  }
};

class Num: public expr {
private:
  int n;
public:
  Num(int p_n) { n = p_n; }
  friend std::ostream &operator<< (std::ostream &out, const Num &num) {
    out << num.n;
  }
};

int main() {

  auto name = Name("x");
  auto num  = Num(5);
  auto lshift = LShift();
  auto binop = BinOp(&name, &num, &lshift);

  std::cout << binop << '\n';

  return 0;
}

So the program mentioned above outputs the pointer,

0x7ffd05935db0 0x7ffd05935d8b 0x7ffd05935d8c

but instead of a pointer, I need the objects needed to print.

x << 5

Solution

  • In general, having operator overloading an polymorphism in the same class is an indication that you're mixing value-style classes and identity-style classes, and thus a C++-specific code smell.

    But I would say that the stream insertion operator is an exception.

    The solution is to overload the operator once, and forward to a function that is more amenable to overriding.

    class AST {
    public:
      virtual void print(std::ostream& s) const = 0;
      virtual ~AST() {} // you probably want this one too
    };
    
    // no need for friendliness
    std::ostream& operator <<(std::ostream& s, const AST& node) {
      node.print(s);
      return s;
    }
    

    And then you put the actual printing logic into the print overrides of each class.

    But I also feel the need to remark that your AST class doesn't seem useful. There is really nothing an operator and a binary expression have in common. It's also conceptually wrong to say that an operator or an expression is an abstract syntax tree. An expression is a part of an AST, a single node in the tree. An operator is a part of an expression. The entire tree is something different.

    You should have separate hierarchy roots for operator_ and expr. And yes, that probably means you have to do the printing stuff twice. It's worth it.