3

考虑这个简单的程序:

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++ 支持协变返回类型,因为如果我希望接口返回 aBaseArea*并且实现返回 a RectangleArea*,那么只要 RectangleArea 派生自 BaseArea 就可以,因为我的合同得到满足。

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

4

6 回答 6

8

因为你未能覆盖

virtual double getArea() = 0;

那不一样

virtual double getArea() const = 0;

这不是同一个功能;两者都可以在一个类定义中共存。

于 2012-07-21T19:55:03.227 回答
8

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

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;
};
于 2012-07-21T19:58:59.493 回答
5

这是因为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-ness 与返回值无关。它影响方法调用的对象,本质上使函数的第一个参数成为指向const对象的指针。并且函数和方法参数控制它的签名(除其他外,比如它的名字,它是否是static,等等)。

你问得更具体:

在同一行中,不是提供非变异函数的实现满足只要求变异函数的接口。

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

对于const在 nonconst也可用时调用版本的情况,请考虑:

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
于 2012-07-21T19:59:45.317 回答
4

大多数答案都说明了为什么根据当前的语言规则不允许这样做,而不是说明为什么规则是这样写的。我将尝试回答为什么规则不能按照您的建议方式进行。

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(“一旦虚函数变为 const,它就不能返回”),但这会使层次结构非常脆弱。添加或删除具有不同常量的函数的中间基类getArea将改变是否Square::getArea()是覆盖或重载。虚函数已经存在一些像这样的脆弱性,尤其是协变返回,但根据 D&E Stroustrup 的说法,协变返回很有用,因为“放松允许人们在类型系统中做一些重要的事情,而不是使用强制转换。” 我不认为允许 const 函数覆盖非 const 函数非常适合类型系统,并且不允许做任何重要的事情,并且不会摆脱强制转换以允许使用新的(安全)技术.

于 2012-07-21T22:00:06.103 回答
1

一个方法可以被重载以同时拥有一个是const和一个不是的版本。(由于返回类型不是方法的唯一签名的一部分,并且方法可能不接受任何参数,const因此可能是区分返回的东西的唯一const X*方法,比如说,或X*。)

如果编译器不要求您对此进行精确处理,那么有人可能会const向基类添加一个非版本,然后您会突然覆盖一个不同的方法。

于 2012-07-21T19:57:18.710 回答
0

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

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

于 2012-07-21T20:02:36.200 回答