Каковы недостатки C ++ ковариантных типов возвращаемых данных? - PullRequest
0 голосов
/ 30 января 2019

Мне недавно приходилось иметь дело с C ++ ковариационными типами возврата , такими как следующая конструкция:

struct Base
{
     virtual ~Base();
};
struct Derived : public Base {};

struct AbstractFactory
{
    virtual Base *create() = 0;
    virtual ~AbstractFactory();
};

struct ConcreteFactory : public AbstractFactory
{
    virtual Derived *create()
    {
        return new Derived;
    }
};

Это позволяет клиентскому коду обрабатывать объект Derived как *Тип 1007 * или тип Derived при необходимости, особенно без использования dynamic_cast или static_cast.

Каковы недостатки этого подхода?Это признак плохого дизайна?

Спасибо.

Ответы [ 2 ]

0 голосов
/ 30 января 2019

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

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

Давайте перепишем наши классы, используя общий и популярный не виртуальный интерфейс идиома.

struct AbstractFactory
{
    Base *create() {
      return create_impl();
    }

  private:
    virtual Base* create_impl() = 0;
};

struct ConcreteFactory : public AbstractFactory
{
    Derived *create() {
      return create_impl();
    }

  private:
    Derived *create_impl() override {
        return new Derived;
    }
};

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

struct ConcreteFactory : public AbstractFactory
{
    Derived *create() {
      return create_impl();
    }

  private:
    Base *create_impl() override {
        return create_impl_derived();
    }

    virtual Derived *create_impl_derived() {
        return new Derived;
    }
};

Теперь и AbstractFactory, и ConcreteFactoryимеет точно такой же интерфейс, как и раньше, без видимого ковариантного типа возврата.Что это значит для нас?Это означает, что мы можем свободно использовать интеллектуальные указатели.

// replace `sptr` with your favourite kind of smart pointer

struct AbstractFactory
{
    sptr<Base> create() {
      return create_impl();
    }

  private:
    virtual sptr<Base> create_impl() = 0;
};

struct ConcreteFactory : public AbstractFactory
{
    sptr<Derived> create() {
      return create_impl();
    }

  private:
    sptr<Base> create_impl() override {
        return create_impl_derived();
    }

    virtual sptr<Derived> create_impl_derived() {
        return make_smart<Derived>();
    }
};

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

Примечаниедля технически склонных.

    sptr<Base> create_impl() override {
        return create_impl_derived();
    }

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

(В данном конкретном случае может быть приемлемым, просто возвращая указатель Base повсюду. Я не обсуждаю это.Я предполагаю, что нам абсолютно необходимо что-то вроде ковариантных типов возврата.)

0 голосов
/ 30 января 2019

Covariance не работает для интеллектуальных указателей, и, как таковая, ковариация нарушает:

Никогда не передавайте владение необработанным указателем (T *) или ссылкой (T &) Основных принципов C ++.,Существуют хитрости для ограничения проблемы, но все же ковариантное значение является необработанным указателем.

Пример из документа:

X* compute(args)    // don't
{
    X* res = new X{};
    // ...
    return res;
}

Это почти то же самое, что и код вВопрос:

virtual Derived *create()
{
    return new Derived;
}

К сожалению, следующее недопустимо как для shared_ptr, так и для unique_ptr:

struct AbstractFactory
{
    virtual std::shared_ptr<Base> create() = 0;
};

struct ConcreteFactory : public AbstractFactory
{
    /*
      <source>:16:38: error: invalid covariant return type for 'virtual std::shared_ptr<Derived> ConcreteFactory::create()'
    */
    virtual std::shared_ptr<Derived> create()
    {
        return std::make_shared<Derived>();
    }
};

EDIT

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

...