1

I have recently had to deal with C++ covariance return types such as the following construct :

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

It allows the client code to treat the Derived object as a Base type or as a Derived type when needed and especially without the use of dynamic_cast or static_cast.

What are the drawbacks of this approach ? Is it a sign of bad design ?

Thank you.

Fryz
  • 2,119
  • 2
  • 25
  • 45
  • 6
    Why do you think there are drawbacks? Why do you think it might be bad design? Please be more specific about your problem. – Lightness Races in Orbit Jan 30 '19 at 11:49
  • 2
    They don't work with smart pointers, so such a design cements the code to use raw pointers. There are tricks to overcome this, to some extent though. – Michael Veksler Jan 30 '19 at 11:57
  • 2
    You also might want to have a virtual destructor. –  Jan 30 '19 at 11:57
  • 1
    The drawback is that the pointer returned needs to be adjusted behind the scenes when virtual function is invoked through a referenced to base `AbstractFactory`. If (a typical situation) virtual function is only invoked though a reference to base class then returning covariant type may cause a potential overhead compared to situation when pointer is adjusted in the function itself. – user7860670 Jan 30 '19 at 11:58
  • 1
    Some people argue that `virtual` calls are slightly slower, but furthermore, I cannot think of anything. https://stackoverflow.com/questions/449827/virtual-functions-and-performance-c – Lourens Dijkstra Jan 30 '19 at 11:58
  • 1
    @LourensDijkstra They are hilariously slow on certain hardware, especially without or with a very small cpu cache. – George Jan 30 '19 at 12:02
  • 1
    Drawback of c++ is that it is limited in covariance return type. We might want to do it with smart pointers too. Else it is perfectly valid C++. – Jarod42 Jan 30 '19 at 12:10

2 Answers2

6

Covariance does not work for smart pointers, and as such covariance violates:

Never transfer ownership by a raw pointer (T*) or reference (T&) of the C++ Core Guidelines. There are tricks to limit the issue, but still the covariant value is a raw pointer.

An example from the document:

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

This is almost the same as what the code in the question is doing:

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

Unfortunately the following is illegal, both for shared_ptr and for 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

n.m.'s answer shows a technique that simulates language covariance, with some additional code. It has some potential maintenance costs, so take that into account before deciding which way to go.

Michael Veksler
  • 8,217
  • 1
  • 20
  • 33
  • I feel like this is likely the biggest consideration here. Factories in modern C++ return `unique_ptr`, period. – Max Langhof Jan 30 '19 at 12:30
  • So, "this design forces you to use raw pointers, which are bad". Yup, fair enough! – Lightness Races in Orbit Jan 30 '19 at 13:27
  • I am not specific about modern C++ here but it's interesting to notice that anyway. – Fryz Jan 30 '19 at 13:39
  • @Scab modern C++ is not that new, it is time to learn it instead of the legacy c++98 or earlier. Anyways, even C++98 has `boost::shared_ptr`, `std::auto_ptr`, or home-made smart-pointers which are the way to go rather than raw pointers. I have been using smart-pointers since 1997, and they should be used even for legacy C++ (there are some issues without move-semantics, and `auto_ptr` is kind of broken, so there are cases when they can't be used). – Michael Veksler Jan 30 '19 at 13:51
  • Covariance is integral to the type erasure technique, where virtual Concept* Concept::clone() is overridden by Model* Model::clone(). This would seem to be a violation of the Core Guidelines too. – arayq2 Jan 30 '19 at 13:59
  • @arayq2 I'm not sure I follow. The type erasure I've used and seen returns the base type. Do you have a link to code or document that strengthens your observation? – Michael Veksler Jan 30 '19 at 14:03
  • "Covariance does not work for smart pointers". This is a language limitation rather than an inherent technique limitation. One can easily implement covariant return types that do work with any kind of handles whatsoever, without relying on the language feature that only works with raw pointers. – n. m. could be an AI Jan 30 '19 at 17:11
2

The chief limitation of covariant return types as implemented in C++ is that they only work with raw pointers and references. There are no real reasons not to use them when possible, but the limitation means we cannot always use them when we need them.

It is easy to overcome this limitation while providing identical user experience, without ever relying to the language feature. Here's how.

Let's rewrite our classes using the common and popular non-virtual interface idiom.

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

Now here something interesting happens. create is no longer virtual, and therefore can have any return type. It is not constrained by the covariant return types rule. create_impl is still constrained, but it's private, no one is calling it but the class itself, so we can easily manipulate it and remove covariance altogether.

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

Now both AbstractFactory and ConcreteFactory has exactly the same interface as before, without a covariant return type in sight. What does it mean for us? It means we can use smart pointers freely.

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

Here we overcame a language limitation and provided an equivalent of covariant return types for our classes without relying on a limited language feature.

Note for the technically inclined.

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

This here function implicitly converts ("upcasts") a Derived pointer to a Base pointer. If we use covariant return types as provided by the language, such upcast is inserted by the compiler automatically when needed. The language is unfortunately only smart enough to do it for raw pointers. For everything else we have to do it ourselves. Luckily, it's relatively easy, if a bit verbose.

(In this particular case it could be acceptable to just return a Base pointer throughout. I'm not discussing this. I'm assuming we absolutely need something like covariant return types.)

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • All is well, but then a class hierarchy needs `create_impl_derived`, `create_impl_derived_derived`, and so on. What in case of virtual multiple inheritance? What if you reactor and add one level in only one of the parents? Possible, but over engineered, and error prone and with maintenance costs. Maybe language-based covariance with raw pointers has fewer issues. Just maybe. – Michael Veksler Jan 30 '19 at 20:42
  • 1
    @MichaelVeksler Did I promise a free ride? – n. m. could be an AI Jan 30 '19 at 20:44
  • I agree, you didn't promise a free ride. I just want my covariance and contravariance in the language, please. – Michael Veksler Jan 30 '19 at 20:45
  • Anyway, I added a reference to your answer at the end of mine. – Michael Veksler Jan 30 '19 at 20:55