Когда C ++ ковариация является лучшим решением? - PullRequest
28 голосов
/ 11 августа 2009

Этот вопрос был задан здесь несколько часов назад и заставил меня понять, что Я никогда не использовал ковариантные типы возвращаемых значений в своем собственном коде. Для тех не уверен, что такое ковариация, это позволяет тип возвращаемого (обычно) виртуального функции различаются при условии, что типы являются частью одного и того же наследования иерархия. Например:

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

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

Различные типы возвращаемых значений двух функций f () называются ковариантными. Более старые версии C ++ требовали, чтобы возвращаемые типы были одинаковыми, поэтому B должен выглядеть следующим образом:

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

Итак, мой вопрос: есть ли у кого-нибудь реальный пример, где требуются ковариантные возвращаемые типы виртуальных функций, или же они предлагают превосходное решение для простого возврата базового указателя или ссылки?

Ответы [ 6 ]

29 голосов
/ 11 августа 2009

Каноническим примером является метод .clone() / .copy(). Так что вы всегда можете сделать

 obj = obj->copy();

независимо от типа объекта.

Редактировать: этот метод клонирования будет определен в базовом классе Object (как на самом деле в Java). Таким образом, если клон не является ковариантным, вам придется либо приводить, либо ограничиваться методами корневого базового класса (у которых будет только очень мало методов по сравнению с классом исходного объекта копии).

12 голосов
/ 11 августа 2009

В общем случае ковариация позволяет вам выразить больше информации в интерфейсе производного класса, чем в интерфейсе базового класса. Поведение производного класса более специфично, чем поведение базового класса, и ковариация выражает (один из аспектов) разницу.

Это полезно, когда у вас есть связанные иерархии 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();
};

Клиенты, которые знают, что у них есть веб-страница (возможно, браузеры), знают, что имеет смысл смотреть на параметры запроса. Клиенты, использующие базовый класс Resource, не знают об этом. Они всегда будут привязывать возвращенную HttpAddress& к переменной URI& или к временной.

Если они подозревают, но не знают, что у их объекта Resource есть HttpAddress, тогда они могут dynamic_cast. Но ковариация превосходит «просто знать» и выполнять приведение по той же причине, что статическая типизация вообще полезна.

Есть альтернативы - прикрепите функцию getQueryParam к URI, но заставьте hasQueryParam вернуть false для всего (загромождает интерфейс URI). Оставьте WebPage::getIdentifier определенным, чтобы возвращать URL&, фактически возвращая HttpIdentifier&, и заставьте вызывающих абонентов делать бессмысленно dynamic_cast (загромождает вызывающий код, и документация WebPage, где вы говорите "возвращаемый URL гарантированно будет динамически castable to HttpAddress "). Добавьте функцию getHttpIdentifier к WebPage (загромождает интерфейс WebPage). Или просто используйте ковариацию для того, что он должен делать, что выражается в том факте, что WebPage не имеет FtpAddress или MailtoAddress, он имеет HttpAddress.

Наконец, есть разумный аргумент, что у вас не должно быть иерархий губбин, не говоря уже о связанных иерархиях губбин. Но эти классы также могут быть интерфейсами с чисто виртуальными методами, поэтому я не думаю, что это влияет на достоверность использования ковариации.

4 голосов
/ 11 августа 2009

Я думаю, что ковариация может быть полезна при объявлении фабричных методов, которые возвращают определенный класс, а не его базовый класс. Эта статья довольно хорошо объясняет этот сценарий и включает в себя следующий пример кода:

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;
    }
    ...
};
1 голос
/ 19 августа 2009

Я часто использую ковариацию при работе с существующим кодом, чтобы избавиться от static_casts. Обычно ситуация похожа на это:

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 голосов
/ 11 августа 2009

Это становится полезным в сценарии, где вы хотите использовать бетонный завод для производства бетонных изделий. Вы всегда хотите самый специализированный интерфейс, который достаточно универсален ...

Код , использующий бетонный завод, может смело предполагать, что продукты являются бетонными продуктами, поэтому он может безопасно использовать расширения, предоставляемые конкретным классом продуктов в отношении абстрактного класса. Это действительно можно считать синтаксическим сахаром, но все равно он сладкий.

0 голосов
/ 11 августа 2009

Другим примером является конкретная фабрика, которая возвращает указатели на конкретные классы вместо абстрактного (я использовал его для внутреннего использования на фабрике, когда фабрике приходилось создавать составные объекты).

...