Когда ковариация С++ - лучшее решение?
Этот вопрос был задан здесь несколько часов назад и заставило меня понять, что
Я никогда не использовал ковариантные типы возврата в своем собственном коде. Для тех
не уверен, что такое ковариация, что позволяет возвращать тип (обычно) виртуального
функции различаются, если типы являются частью одного и того же наследования
иерархия. Например:
struct A {
virtual ~A();
virtual A * f();
...
};
struct B : public A {
virtual B * f();
...
};
Различные типы возврата двух функций f() называются ковариантными. Старые версии С++ требовали, чтобы возвращаемые типы были одинаковыми, поэтому B должен выглядеть следующим образом:
struct B : public A {
virtual A * f();
...
};
Итак, мой вопрос: есть ли у кого-нибудь реальный пример, где требуются ковариантные типы возвращаемых виртуальных функций или выработка превосходного решения для простого возврата базового указателя или ссылки?
Ответы
Ответ 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();
Теперь, когда вы сталкиваетесь с таким кодом, можно спорить об оригинальном дизайне и о том, есть ли лучшее - но по моему опыту часто существуют такие ограничения, как приоритеты, затраты/выгоды и т.д., которые мешают обширной дизайнерской благоустройству и позволяют малые шаги, такие как этот.