C++中的切片和运算符重载

6

背景信息

我已经用Java编程有一段时间了,仅在几个月前才切换到C++,所以如果我的答案只是我错过的愚蠢问题,请原谅!现在说完这些,是时候来看看手头的问题了!我正在开发一个基本的文本游戏引擎,最近遇到了一个有趣而特定的问题。我尝试在下面的程序中测试它,在屏幕上繁琐的代码可能让人眼花缭乱,而且问题也变得更加复杂,所以我决定只展示这个程序(而非我实际的游戏代码)。下面模拟的问题与我的实际代码中的问题相似,只是没有那些无关的因素。

问题描述

根本问题是多态性的问题。我想将输出运算符"<<"重载为一个独特的显示函数,以服务于继承层次结构中的每个对象。问题是,当我从存储这些继承成员的列表中调用这个运算符时,它们会失去自己的身份,并调用基类的输出运算符。通常,解决这个问题的方法是用一个简单的显示方法替换运算符重载,标记这个显示方法为虚方法,然后继续开心地做下去。我不介意对代码进行这样的修改,但现在我只是好奇,有没有一种方法可以重载继承层次结构中的运算符,以满足我的要求?

[示例] 代码

#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;
    }
}

[示例] 代码的输出结果

Polygon!
Polygon!
Polygon!

[示例] 代码的期望输出结果

Polygon!
Rectangle!
Square!

2
顺便说一句,那是一个很好的问题。大多数初学者都会问一些糟糕的问题。 - Puppy
由于 operator<< 是友元函数(而不是成员函数),所以简短的答案是否定的,你需要一个内部的显示运算符,并将其标记为虚拟的。 - vsoftco
既然你是Java程序员,你应该知道这是C++中的内存泄漏,除非你释放了内存:listOfPoly.push_back(new Polygon());... 最好使用智能指针。 - PaulMcKenzie
“Square” 是 “Rectangle” 是 “Polygon”?认真的吗?请先了解一下 Liskov 替换原则。 - D Drmmr
3个回答

5

有没有一种方法可以在继承层次结构中重载运算符,以达到我想要的效果?

没有。

问题在于这些运算符并不在你的继承层次结构中。这里的friend关键字只是前向声明一个自由函数,并赋予其对类的特权访问权限。它并不是一个方法,因此不能将其设置为虚拟的。


请注意,运算符重载只是语法糖。表达式

os << shape;

可以解析为自由函数(就像您在这里所做的那样)

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

或者左操作数的成员,例如
ostream& ostream::operator<<(Polygon&);

显然,这里第二种情况不存在,因为你必须修改std::ostream。语法无法解析的是右操作数的成员。

因此,您可以拥有一个自由函数运算符(必须是非虚函数),或者左手操作数上的方法(可能是虚函数),但不能是右操作数上的方法。


通常的解决方案是在层次结构的顶层具有单个重载,它分派到一个虚方法。所以:

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

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

现在只需实现Polygon :: format,并在派生类中覆盖它。
顺便提一下,使用friend的代码味道很不好。通常认为最好的样式是给您的类一个完整的公共接口,以便外部代码无需特权访问即可与其一起工作。
进一步偏离背景信息:多个调度是一件事,当所有参数类型在静态上下文中都已知时,C ++重载解析可以很好地处理它。未处理的是在运行时发现每个参数的动态类型,然后尝试找到最佳重载(如果您考虑多个类层次结构,则显然非常棘手)。
如果您用编译时多态元组替换运行时多态列表,并在其上进行迭代,则原始重载方案将正确分派。

1
顺便提一下,那个流插入器可以是一个inline friend函数。(除非它根本不需要成为friend函数,否则没有理由不这样做。) - Deduplicator
“friend”并不是代码异味。虽然使用“friend”不是最常见的语言特性,但这是一个很好的例子,可以证明它是完全合理的。 - Puppy

3
运算符不是虚成员。这意味着它不可能派发到派生类。只有虚函数才能动态分派。在这种情况下的典型策略是创建一个普通运算符,它将分派到接口上的虚函数来执行工作。
顺便说一下,在 C++ 中,new 是一个几乎没有用的语言特性。你需要发现智能指针,否则你写的每一行代码都会为无尽的生命周期问题而重新编写。
通常,使运算符成为虚拟的是一个非常糟糕的想法。这是因为你只能在 this 上动态分派,但运算符经常与实现它们的类型在 RHS 上或作为非成员使用。非成员运算符重载比成员更强大。

1
你可以在基类Rectangle中添加一个虚拟的Display()函数。层次结构中的每个类都可以重写该函数并以不同的方式实现它。
然后,你只需要定义一个以Polygon&为参数的operator<<,该函数本身只调用虚拟的Display函数。
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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接