C++虚函数的返回类型

92

一个继承类能否使用不同的返回类型来实现虚函数(不使用模板作为返回值)?

3个回答

97

在某些情况下,派生类可以使用不同的返回类型覆盖虚函数,只要返回类型与原始返回类型 协变 即可合法。例如,考虑以下情况:

class Base {
public:
    virtual ~Base() {}

    virtual Base* clone() const = 0;
};

class Derived: public Base {
public:
    virtual Derived* clone() const {
        return new Derived(*this);
    }
};

在这里,Base 定义了一个纯虚函数叫做 clone,它返回一个 Base *。在派生类的实现中,这个虚函数被重载并使用了一个返回类型为 Derived *。尽管返回类型与基类不同,但这是完全安全的,因为任何时候你使用到该函数时也会写

Base* ptr = /* ... */
Base* clone = ptr->clone();

调用clone()将始终返回指向Base对象的指针,即使它返回Derived*指针,该指针也会隐式转换为Base*并且该操作是定义良好的。

更一般地,函数的返回类型从不被视为其签名的一部分。只要返回类型是协变的,就可以使用任何返回类型覆盖成员函数。


12
这句话“您可以使用任何返回类型来覆盖成员函数”是错误的。只要返回类型相同或者协变(正如您所解释的那样),您就可以进行覆盖。这里没有更通用的情况。 - bronekk
2
@bronekk- 你引用的句子的其余部分指出,新的返回类型必须可在原始类型可用的任何地方使用;也就是说,新类型与原始类型是协变的。 - templatetypedef
那个句子的其他部分不正确;想象将“Base”替换为“long”,将“Derived”替换为“int”(或者反过来,无所谓),这是行不通的。 - bronekk
1
类型可在原始类型可用的任何地方使用和协变性是两个不同的上下文。 协变 意味着实现函数的类型之间的关系和返回类型之间的关系以相同的方式 变化逆变(虽然在 C++ 中无用)是相反的情境。一些语言允许动态分派中的逆变参数(如果基类接受类型为 T 的对象,则派生类型可以接受 T',其中 T' 是 T 的基类--当您向下移动一个层次结构时,您向上移动另一个)。 - David Rodríguez - dribeas
1
有人指出了你回答中的一个错误(现在已经是5年前的事了!)你能否编辑一下,而不是把更正留在评论中呢? :) - Yakk - Adam Nevraumont
@Yakk 完成了! 那是很久以前的事情,当时我没有意识到那是礼节。 :-) - templatetypedef

56
是的。只要它们是协变的,返回类型允许不同。C++标准描述如下(§10.3/5):
覆盖函数的返回类型必须与被覆盖函数的返回类型相同或与函数类的协方差。如果函数D::f覆盖函数B::f,则函数的返回类型在满足以下条件时协变:
- 两者都是指向类或引用类的指针98) - B::f的返回类型中的类与D::f的返回类型中的类相同,或者是D::f的返回类型中的类的明确直接或间接基类,并且在D中是可访问的。 - 两个指针或引用具有相同的cv限定符,且D::f的返回类型中的类类型具有与B::f的返回类型中的类类型相同的cv限定符或更少的cv限定符。
注脚98指出,“不允许多级指向类的指针或多级指向类的引用”。
简而言之,如果DB的子类型,则D中函数的返回类型需要是B中函数的返回类型的子类型。最常见的例子是当返回类型本身基于DB时,但它们不一定是基于它们。考虑以下情况,其中我们有两个独立的类型层次结构:
struct Base { /* ... */ };
struct Derived: public Base { /* ... */ };

struct B {
  virtual Base* func() { return new Base; }
  virtual ~B() { }
};
struct D: public B {
  Derived* func() { return new Derived; }
};

int main() {
  B* b = new D;
  Base* base = b->func();
  delete base;
  delete b;
}

这种方法有效的原因是任何调用func的人都期望得到一个Base指针。任何Base指针都可以。因此,如果D::func承诺始终返回一个Derived指针,则它将始终满足祖先类所制定的契约,因为任何Derived指针都可以隐式转换为Base指针。因此,调用者总会得到他们所期望的结果。
除了允许返回类型不同外,一些语言还允许覆盖函数的参数类型也可以不同。当它们这样做时,通常需要是反变的。也就是说,如果B::f接受一个Derived*,那么D::f将被允许接受一个Base*。后代可以在接受的内容上更加宽松,在返回的内容上更加严格。C++不允许参数类型的反变。如果您改变参数类型,C++会认为它是一个全新的函数,因此您会开始进行重载和隐藏。有关此主题的更多信息,请参见维基百科中的Covariance and contravariance (computer science)

2
这是一个实际的特性还是由于未在解析中使用返回类型而产生的副作用? - Martin York
1
@Martin,绝对是一个特性。我很确定重载决议与此无关。如果你正在覆盖一个函数,返回类型被使用。 - Rob Kennedy

3

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