Подтвердить что ты не робот

Когда ковариация С++ - лучшее решение?

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

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

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

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

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

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

4b9b3361

Ответ 1

Канонический пример - это метод .clone()/.copy(). Поэтому вы всегда можете

 obj = obj->copy();

вне зависимости от типа obj.

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

Ответ 2

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

Это полезно, когда у вас есть связанные иерархии губбин, в ситуациях, когда некоторые клиенты захотят использовать интерфейс базового класса, но другие клиенты будут использовать интерфейс производного класса. Если const-correctness опущена:

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();
};

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

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

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

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

Ответ 3

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

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;
    }
    ...
};

Ответ 4

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

Ответ 5

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

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

Ответ 6

Я часто использую ковариацию при работе с существующим кодом, чтобы избавиться от 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();

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