为什么 const 方法不覆盖 C++ 中的非 const 方法?

Why doesn't a const method override a non-const method in C++?

本文关键字:const 方法 覆盖 为什么 C++      更新时间:2023-10-16

考虑这个简单的程序:

class Shape
{
public:
    virtual double getArea() = 0;
};
class Rectangle : public Shape
{
    int width;
    int height;
public:
    Rectangle( int w , int h ) :width(w) , height(h) {}
    double getArea() const
    {
        return width * height;
    }
};

int main() {
    Rectangle* r = new Rectangle(4,2);
}

尝试编译这个问题给了我:

 'Rectangle' : cannot instantiate abstract class

为什么在C++当协变返回类型为时不允许这样做?当然,我可以通过Rectangle::getArea成为一个非常量函数来修复程序,但我很好奇为什么语言设计者会做出其他决定。

编辑

很多人在他们的回答中提到签名有何不同。但事实也是如此

class Shape
{
public:
    virtual BaseArea* getArea() = 0;
};
class Rectangle : public Shape
{
public:
    virtual RectangleArea* getArea();
};

但是当 C# 不允许时,C++会竭尽全力允许它。

C++支持协变返回类型,因为如果我希望接口返回BaseArea*并且实现返回RectangleArea*,只要 RectangleArea 从 BaseArea 派生就可以了,因为我的合同得到了满足。

同样,提供非变异函数的实现不是满足只要求变异函数的接口吗?

在这种情况下会发生什么:

struct base
{
    virtual void foo(); // May implement copy-on write
    virtual void foo() const;
};
struct derived : base
{
    // Only specialize the const version, the non const
    // default suits me well. How would I specify that I don't
    // want the non-const version to be overriden ?
    void foo() const;
};

因为您未能覆盖

virtual double getArea() = 0;

那不一样

virtual double getArea() const = 0;

它不是同一个函数;两者都可以在单个类定义中共存。

大多数答案都说为什么在语言的当前规则方面不允许这样做,而不是说为什么规则是这样写的。 我会试着回答为什么规则不可能是你建议的方式。

Stroustrup的《设计与进化C++》一书描述了为什么放宽了压倒一切的规则,以允许协变返回,这在C++中并不总是允许的。 因此,对您的问题的一个答案是,最初的覆盖必须与签名完全匹配,并且对于不会削弱虚函数合约的"兼容"返回类型例外。他们可能只是没有进一步放松,因为没有人想到它或没有人建议它。 D&E确实提到了其他可能放宽覆盖规则,但表示"我们认为,通过覆盖允许此类转换的好处不会超过实施成本和混淆用户的可能性。 这是相关的,因为我认为你的想法有很多可能让用户感到困惑,并且实际上会导致安全问题,特别是它削弱了类型系统。

考虑:

class Square : public Rectangle
{
public:
  explicit Square(int side) : Rectangle(side, side) { }
  virtual double getArea() // N.B. non-const, overrides Shape::getArea
  {
    // class author decides this would be a sensible "sanity check"
    // (I'm not suggesting this is a good implementation)
    if (height != width)
      height = width;
    return Rectangle::getArea();
  }
};
const Square s(2);
int main()
{
  double (Rectangle::*area)() const = &Rectangle::getArea;
  double d = (s.*area)();
}

我相信你的想法会使这段代码有效,在 const 对象上调用 const 成员函数,但实际上它是一个虚函数,所以它调用Square::getArea()这是非 const,因此它试图修改一个 const 对象,它可以存储在只读内存中,因此会导致段错误。

这只是一个例子,

说明在Shape示例中允许覆盖放宽可能会导致未定义的行为,我相信在更现实的代码中可能存在更大,也许更微妙的问题。

你可以争辩说编译器不应该允许非常量函数覆盖Rectangle::getArea因此应该拒绝Square::getArea("一旦虚拟函数变得常量,它就不能回去"(,但这会使层次结构非常脆弱。添加或删除具有不同常量getArea函数的中间基类将更改Square::getArea()是重写还是重载。虚函数已经存在一些脆弱性,尤其是协变返回,但根据D&E Stroustrup认为协变返回很有用,因为"松弛允许人们在类型系统中做一些重要的事情,而不是使用强制转换。 我不认为允许 const 函数覆盖非 const 函数非常适合类型系统,并且不允许做任何重要的事情,并且不会摆脱强制转换以允许使用新的(安全(技术。

这是因为

const方法与非const方法具有不同的签名。因此,编译器正在寻找未实现的方法。据推测,非const版本可以做一些与const版本完全不同的事情。但是,如果这是您想要的语义,则可以轻松提供它:

class Shape
{
public:
    virtual double getArea() {
        return static_cast<const Shape *>(this)->getArea();
    }
    virtual double getArea() const = 0;
};

在编辑中,您提供了协变返回类型的示例:

C++支持协变返回类型,因为如果我希望接口返回 BaseArea* 和返回为 RectangleArea* 的实现,那么如果 RectangleArea 派生自 BaseArea,因为我的合同得到了满足。

方法或函数的返回值从来都不是其签名的一部分。方法的const性与返回值无关。 它影响方法调用的对象,实质上使函数的第一个参数成为指向const对象的指针。函数和方法参数控制它的签名(除其他外,比如它的名字,它是否static,等等(。

你问得更具体:

同样,也不是提供非变异函数的实现,该函数满足仅请求变异函数的接口。

即使该语言可以(我认为它实际上在某些情况下确实(解析为const版本,它仍然需要为非const版本提供实现,因为这是声明为纯虚拟的方法。

对于在非const也可用时调用const版本的情况,请考虑以下事项:

class Shape {
public:
    virtual double getArea() {
        std::cout << "non-const getArea" << std::endl;
        return static_cast<const Shape *>(this)->getArea();
    }
    virtual double getArea() const = 0;
};

再加上示例中的Rectangle。然后:

Rectangle *r = new Rectangle(4,2);
Shape *s = r;
r->getArea(); // calls const version
s->getArea(); // call non-const version

可以重载方法以同时具有const版本和未版本。 (由于返回类型不是方法的唯一签名的一部分,并且方法可能不接受任何参数,因此const可能是区分返回的内容(例如const X*X*(的唯一方法。

如果编译器不要求您对此进行精确处理,则有人可以将非const版本添加到基类中,然后突然间您将重写不同的方法。

已经提到了常量部分,所以我跳过了这个。假设您将基类视为要确保履行的协定。由于函数的常量和非常量版本可以共存,并且应该协同执行,如果您不履行给定的合同,则应告知您。

想象一个具有 const 和非 const 方法的基类,例如,一个容器,其中两种风格都有 operator[]。现在,您继承但不提供这两个函数。您的孩子不履行合同,不提供所需的功能。所以你应该得到一个错误,因为你的孩子可能无法通过多态性使用