C++协变性何时是最佳解决方案?

36

这个问题 几小时前在这里被提出,让我意识到我从来没有在我的代码中使用过协变返回类型。对于那些不确定 covariance 是什么的人,它允许 (通常是) 虚函数的返回类型不同,只要这些类型是同一继承层次结构的一部分。例如:

struct A {
   virtual ~A();
   virtual A * f();
   ...
};

struct B : public A {
   virtual B * f();
   ...
};
两个 f() 函数的不同返回类型被称为协变。C++ 的旧版本要求返回类型必须相同,因此 B 必须像这样:

两个 f() 函数的不同返回类型被称为协变。C++ 的旧版本要求返回类型必须相同,因此 B 必须像这样:

struct B : public A {
   virtual A * f();
   ...
};

所以,我的问题是:有没有什么现实世界的例子需要虚函数的协变返回类型,或者协变返回类型可以产生比仅仅返回基类指针或引用更好的解决方案?


当我读到这个问题时,我的反应也完全一样。问题出在哪里 - 不要使用协变返回类型! - Richard Corden
6个回答

37

经典的例子是使用.clone()/.copy()方法。这样你就可以随时进行操作。

 obj = obj->copy();

无论 obj 的类型是什么。

编辑:这个克隆方法将在 Object 基类中定义(实际上在 Java 中就是这样)。因此,如果克隆方法不是协变的,你要么必须进行强制转换,要么只能使用根基类的方法(与复制源对象的类相比,该基类只有非常少的方法)。


2
但是(在我看来)你不应该关心类型,A * a = some_a_or_b->clone();同样有效,然后您可以在克隆的指针上使用其他虚拟方法。 - anon
2
我认为协变更多地是一种语法糖,而不是必需的功能。你总是可以做 A* a = b->clone(); dynamic_cast<B*>( a )->initB(); - Christopher
11
除了使用协方差外,这种类型是静态检查的。 - AProgrammer
4
在我看来,任何类型的转型都很难看且不够简洁,应尽可能避免使用。在这里使用 dynamic_cast,编译时没有检查 B::clone 是否确实执行了方法名所示并返回一个 B*(而不是 A 的其他子类指针)。 - Tyler McHenry
3
这个例子在Java的类型系统有问题时是有意义的,但我仍然看不出在C++中的相关性。 - jalf
显示剩余4条评论

17

通常情况下,协变性允许您在派生类接口中表达比基类接口更多的信息。派生类的行为比基类更具体,而协变性表达了(一个方面的)差异。

当您拥有相关层次结构的gubbins时,它非常有用,在某些客户端将使用基类接口,但其他客户端将使用派生类接口的情况下。

class URI { /* stuff */ };

class HttpAddress : public URI {
    bool hasQueryParam(string);
    string &getQueryParam(string);
};

class Resource {
    virtual URI &getIdentifier();
};

class WebPage : public Resource {
    virtual HttpAddress &getIdentifier();
};
已知具有Web页面的客户端(例如浏览器)知道查看查询参数的意义。而使用Resource基类的客户端则不知道这一点。他们将始终将返回的HttpAddress&绑定到URI&变量或临时变量中。
如果他们怀疑但不确定他们的Resource对象具有HttpAddress,则可以使用dynamic_cast。但是,协变性比“只知道”并进行转换更优越,原因与静态类型在所有情况下都有用的原因相同。
有替代方案 - 将getQueryParam函数放在URI上,但使hasQueryParam对所有内容返回false(会使URI接口混乱)。将WebPage :: getIdentifier定义为返回URL&,实际上返回HttpIdentifier&,并让调用者执行无意义的dynamic_cast(会使调用代码和WebPage的文档混乱,其中您说“返回的URL保证可以动态转换为HttpAddress”)。在WebPage中添加一个getHttpIdentifier函数(会使WebPage接口混乱)。或者只是将covariance用于其旨在表达的事实,即WebPage没有FtpAddressMailtoAddress,而是有一个HttpAddress
最后当然有一个合理的观点,即您不应该有关系层次结构,更不用说相关的关系层次结构了。但是,这些类也可以是具有纯虚方法的接口,因此我不认为这影响使用covariance的有效性。

2
gubbins?这不是我熟悉的词。在UrbanDictionary.com上搜索它,得到以下结果:Gubbin是指偶尔表现得像流浪汉的人。 “你不会相信他的父母拥有一座豪宅,是吗?那个Gubbin!”我推测在编程环境中有不同的含义吗? - Jeremy Friesner
2
恐怕不是。它只是指“东西”,“事情”。 - Steve Jessop

6

我认为协方差可以在声明工厂方法时很有用,这些方法返回的是一个特定类而不是它的基类。这篇文章很好地解释了这种情况,并包含了以下代码示例:

class product
{
    ...
};

class factory
{
public:
    virtual product *create() const = 0;
    ...
};

class concrete_product : public product
{
    ...
};

class concrete_factory : public factory
{
public:
    virtual concrete_product *create() const
    {
        return new concrete_product;
    }
    ...
};

2

在使用现有代码时,我经常使用协变来消除static_cast。通常情况类似于这样:

class IPart {};

class IThing {
public:
  virtual IPart* part() = 0;
};

class AFooPart : public IPart {
public:
  void doThis();
};

class AFooThing : public IThing {
  virtual AFooPart* part() {...}
};

class ABarPart : public IPart {
public:
  void doThat();
};

class ABarThing : public IThing {
  virtual ABarPart* part() {...}
};    

这使我能够:
AFooThing* pFooThing = ...;
pFooThing->Part()->doThis();

并且

ABarThing pBarThing = ...;
pBarThing->Part()->doThat();

代替
static_cast< AFooPart >(pFooThing->Part())->doThis();

并且

static_cast< ABarPart >(pBarThing->Part())->doThat();

现在,当遇到这样的代码时,人们可以对原始设计进行争论,以确定是否存在更好的设计 - 但根据我的经验,常常会有各种限制,如优先级、成本/效益等,它们会干扰彻底的设计优化,并且只允许进行小的改进,比如这个。

0
另一个例子是具体工厂会返回指向具体类的指针,而不是抽象类(我在工厂内部使用它来构建复合对象时也用过)。

你可能使用dynamic_cast指针来确定你实际拥有的具体实例?如果是这样,你就不需要协变了。或者我漏掉了什么? - anon
'dynamic_cast' 要求类型是多态的,而且可能很昂贵。然而,可以使用工厂类中的函数模板来实现相同的结果。这将执行两个任务:检查类型是否相关(isbaseclass或其他)然后将通用结果向下转换为派生类型。不需要动态转换。 - Richard Corden
我所说的“潜在昂贵”是指比static_cast更昂贵,即使如此,只有在您调用它数百万次时才会有所区别。 ;) - Richard Corden
你也可以想象一种类似于访问者模式的模式。但在我使用的情况下,协变是在工厂内部使用的。显然,工厂知道自己的动态类型(我还没有在工厂中使用两个继承级别:-()。 - AProgrammer
2
@Richard:我想Scott Meyers曾经指出在使用dynamic_cast 在VS中会将你降级到字符串比较。(如果我没记错的话;这是当涉及到DLLs时。)这肯定比static_cast慢几个数量级。 - sbi

0

它在你想要使用具体工厂生成具体产品的场景中变得有用。你总是希望使用最专业的接口,而这个接口又足够通用...

使用具体工厂的代码可以安全地假设产品是具体产品,因此它可以安全地使用具体产品类提供的扩展来处理抽象类。这确实可以被视为语法糖,但无论如何都很甜。


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