Slicing and operator overloading in C++
Asked Answered
C

3

6

Background Info

I've been programming in Java for a while, and I've only switched over to C++ just a few months ago, so I apologize if the answer is just something silly that I missed! Now that that's all been said, it is time for the issue at hand! I am developing a basic text-based game engine, and I recently ran into an interestingly specific and unlikely problem. I tried testing it out on a smaller scale in the program below, and decided to just show that (as opposed to my actual game code), as to not choke the screen, and make the issue less convoluted. The problem modeled below mirrors the problem with my actual code, just without the fluffy distractors.

The Problem

Essentially the problem is one of polymorphism. I want to overload the output operator "<<" to serve as a display function that is unique for each object in the hierarchy. The issue is that when I call this operator from a list which stores these hierarchy members, they lose their identity and call the output operator of the base class. Normally, one would solve this by replacing the operator overloads with a simple display method, marking the display method virtual, and moving on with their happy day. I don't particularly mind making that alteration to the code, but now I'm just plain curious. Is there a way to overload operators in a hierarchy that results in what I'm going for here?

The [Example] Code

#include <vector>
#include <iostream>
#include <string>

using namespace std;

class Polygon {
    friend ostream& operator<<(ostream& os, const Polygon& p);
public:

private:

};


class Rectangle : public Polygon {
    friend ostream& operator<<(ostream& os, const Rectangle& r);
public:

private:

};


class Square : public Rectangle {
    friend ostream& operator<<(ostream& os, const Square& s);
public:

private:

};

ostream& operator<<(ostream& os, const Polygon& p) {
    os << "Polygon!" << endl;
    return os;
}
ostream& operator<<(ostream& os, const Rectangle& r) {
    os << "Rectangle!" << endl;
    return os;
}
ostream& operator<<(ostream& os, const Square& s) {
    os << "Square!" << endl;
    return os;
}


int main() {
    vector<Polygon*> listOfPoly;
    listOfPoly.push_back(new Polygon());
    listOfPoly.push_back(new Rectangle());
    listOfPoly.push_back(new Square());

    for(Polygon* p : listOfPoly) {
        cout << *p;
    }
}

Output for [Example] Code

Polygon!
Polygon!
Polygon!

Desired output for [Example] Code

Polygon!
Rectangle!
Square!
Calliopsis answered 19/11, 2014 at 19:28 Comment(4)
By the way, that's a great question. Most first-timers ask such crappy questions.Stigma
as operator<< is a friend (and not a member), the short answer is no, you need an internal display operator that's marked virtualCrymotherapy
Since you are a Java programmer, you do know that this is a memory leak in C++ unless you deallocate the memory: listOfPoly.push_back(new Polygon());... Prefer to use smart pointers instead.Jat
Square IS-A Rectangle IS-A Polygon? Seriously? Please read up on the Liskov substitution principle.Bolitho
D
5

Is there a way to overload operators in a hierarchy that results in what I'm going for here?

No.

The problem is, the operators aren't in your hierarchy. The friend keyword here just forward-declares a free function and gives it privileged access to the class. It doesn't make it a method, so it can't be virtual.


Note that operator overloading is just syntactic sugar. The expression

os << shape;

can either resolve to the free function (as you have here)

ostream& operator<< (ostream&, Polygon&);

or a member of the left-hand operand, such as

ostream& ostream::operator<<(Polygon&);

(obviously here the second case doesn't exist, because you'd have to modify std::ostream). What the syntax can't resolve to is a member of the right-hand operand.

So, you can have a free function operator (which is necessarily non-virtual), or a method on the left hand operand (which could be virtual), but not a method on the right operand.


The usual solution is to have a single overload for the top-level of the hierarchy which dispatches to a virtual method. So:

class Polygon {
public:
  virtual ostream& format(ostream&);
};

ostream& operator<<(ostream& os, const Polygon& p) {
    return p.format(os);
}

Now just implement Polygon::format, and override it in the derived classes.


As an aside, using friend carries a code smell anyway. In general it's considered better style to give your class a public interface complete enough that external code doesn't need privileged access to work with it.

Further digression for background info: multiple dispatch is a thing, and C++ overload resolution handles it fine when all the argument types are known statically. What isn't handled is discovering the dynamic type of each argument at runtime, and then trying to find the best overload (which, if you consider multiple class hierarchies, is obviously non-trivial).

If you replace your runtime-polymorphic list with a compile-time polymorphic tuple and iterate over that, your original overloading scheme would dispatch correctly.

Dalliance answered 19/11, 2014 at 19:37 Comment(2)
As an aside, that stream-inserter can be an inline friend function. (And there's no reason it shouldn't be, unless it need not be a friend-function at all.)Gondolier
friends are not code smells at all. It's true that friend isn't the most common language feature to use, but this is one example of when it's quite justified.Stigma
S
3

The operator is not a virtual member. This means that it's impossible for it to dispatch to the derived class. Only virtual functions can dispatch dynamically. A typical strategy in this scenario is to create a normal operator which dispatches to a virtual function on the interface to perform the work.

By the way as an added bonus, new is a pretty much useless language feature in C++. You need to discover smart pointers, else every line of code you write you'll just have to re-write for endless lifetime problems.

Making operators virtual is a pretty bad idea, usually. This is because you can only dynamically dispatch on this but operators are frequently used with the type implementing them on the RHS, or as non-members. Non-member operator overloads are more powerful than members.

Stigma answered 19/11, 2014 at 19:34 Comment(0)
M
1

You could add a virtual Display() function in the base class Rectangle. Each class in the hierarchy can override the function and implement it differently.

You then only need to define one operator<< that takes a Polygon& as parameter. The function itself just calls the virtual Display function.

class Polygon
{
public:     
  virtual void Display(ostream& os) const 
  { 
      os << "Polygon" << endl; 
  }
};

class Rectangle : public Polygon
{
public:
  virtual void Display(ostream&os) const override
  { 
      os << "Rectangle" << endl; 
  }
};

ostream& operator<<(ostream& os, const Polygon& p) 
{
    p.Display(os);
    return os;
}
Minestrone answered 19/11, 2014 at 19:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.