Search code examples
c++c++11polymorphismabstract-class

Polymorphic return type for child classes


I want to make an abstract class with a pure virtual function process.

  • process should return a variable of type Result:
class Base {
public:
    class Result {
    public:
        int a = 1;
        virtual ~Result() {} // Virtual destructor
    };
    virtual Result process() = 0; // Pure abstract function
};

My idea (maybe not the right idea?) is that child classes of Base should also re-implement Result if they want to add information:

class Child : public Base {
public:
    class ResultChild : public Base::Result  {
    public:
        int b;
    };
    
    Base::Result process() override {
        std::cout << "Processing in Child class" << std::endl;
        ResultChild  result;
        result.a = 7;
        result.b = 2;
        return result;
    }
};

I tried calling the chlid class as:

int main() {
    Child child;
    auto result = child.process(); // Does result even know the member b here?
    std::cout << "a: " << result.a << std::endl; // this is 7
    //std::cout << "b: " << result.b << std::endl; //‘class Base::Result’ has no member named ‘b’
    // Child::ResultChild poly = dynamic_cast<Child::ResultChild>(result); // ‘class Child::ResultChild’ (target is not pointer or reference)
    Child::ResultChild* poly = dynamic_cast<Child::ResultChild*>(&result); // warning: can never succeed
    return 0;
}

I added the errors and warnings from my compiler in the commented code.

  1. Is there an actual way to make the right cast?
  2. Was the member b lost when returning?
  3. Is this just a bad idea?

Ps. This is a simplified version of my code, in real life Base is VideoPipeline and forces it's children to implement process(image) just that some child classes may add more information than others (including types that are not available in all children classes).


Solution

  • You are experiencing something known as "object slicing". When you have two types:

    struct Base {
      int x;
    };
    struct Derived:Base {
      int y;
    };
    

    you are free to take a Derived object and assign it to (or copy it to) a Base. When you do so, however, the extra information in Derived is "sliced" off - the part of Derived that is a base is "sliced" off from the body of Derived.

    C++, unlike most of the OO languages today, allows you to have actual values of object type. This is confusing to many people used to other languages.

    Actual values of an object type must fit within a known memory footprint, because they actually exist where you declare them - they aren't references to an object stored somewhere else, like is common in most other OO supporting languages.

    When you have a value of type X in C++, you actually have a value of type X. You cannot have a derived type in that variable.

    On the other hand, if you have a pointer or reference to a value of type X, you could be pointing to or referring to a derived type of X.

    A second possible source of confusion is your use of auto. auto doesn't mean "polymorphic", it means "work out the type I should put here based on what type the expression is".

    auto result = child.process();
    

    This is identical to declaring result to be the return type of process():

    Base::Result process() 
    

    aka Base::Result.

    So we get

    Base::Result result = child.process();
    

    and result is and only is a variable of the type Base::Result. It cannot be a derived type.

    So, that is what is going on syntactically. You can do what you want semantically by using a different syntax.

    The first, easiest solution is to use smart pointers

    virtual std::unique_ptr<Result> process() = 0;
    

    A unique_ptr is a possibly-empty owner of an object of type Result or derived from Result.

    std::unique_ptr<Base::Result> process() override {
        std::cout << "Processing in Child class" << std::endl;
        auto result = std::make_unique<ResultChild>();
        result->a = 7;
        result->b = 2;
        return std::move(result);
    }
    

    now we return a smart pointer. This smart pointer can refer to a different type, as it is a pointer.

    It acts like a (move-only) value as well.

    Child child;
    auto pResult = child.process(); // Does result even know the member b here?
    if (!pResult)
      return -1;
    std::cout << "a: " << result->a << std::endl; // this is 7
    Child::ResultChild& poly = dynamic_cast<Child::ResultChild&>(*pResult);
    std::cout << "b: " << poly.b << std::endl;
    return 0;
    

    this specific dynamic_cast will throw if it fails to succeed. You could also:

    Child::ResultChild* poly = dynamic_cast<Child::ResultChild*>(pResult.get());
    

    which puts a nullptr into poly if it fails.

    We can use more advanced technique than this. My personal favourite is augmenting std::any with a known-base. A sketch of the pseudo code looks like:

    template<class Base>
    struct poly_any:std::any {
      poly_any() {}
      poly_any(std::nullptr_t) {}
    
      poly_any(poly_any&&)=default;
      poly_any(poly_any const&)=default;
      poly_any& operator=(poly_any&&)& =default;
      poly_any& operator=(poly_any const&)& =default;
      ~poly_any() = default;
    
      poly_any( std::derived_from<Base>&& derived ):
        std::any(std::move(derived)),
        pconvert(+[](std::any const& self)->Base const&{
          std::any const* pAny = this;
          return std::any_cast<Base>(pAny);
        })
      {}
    
      Base const* get() const& {
        return pconvert(*this);
      }
      Base* get() & {
        return const_cast<Base*>(pconvert(*this));
      }
    
      Base const& base() const& { return *get(); }
      Base& base()& { return *get(); }
      Base&& base()&& { return static_cast<Base&&>(base()); }
    
      explicit operator bool() const {
        return this->has_value() && pconvert;
      }
      Base* operator->(){ return get(); }
      Base const* operator->() const { return get(); }
    
    private:
      Base const*(*pconvert)(any const&) = nullptr;
    };
    

    what this is is a (nullable) value-type that stores anything derived from Base.

    Using this your code becomes:

    virtual poly_any<Result> process() = 0; // Pure abstract function
    

    and

    poly_any<Base::Result> process() override {
    

    and the use code becomes:

    Child child;
    auto result = child.process(); // Does result even know the member b here?
    if (!result) return -1;
    std::cout << "a: " << result->a << std::endl; // this is 7
    Child::ResultChild* poly = dynamic_cast<Child::ResultChild*>(result.get()); // warning: can never succeed
    if (!poly) return -2;
    std::cout << "b: " << poly->a << std::endl; // this is 7
    

    the difference here is that unique_ptr is a uniquely-owned move-only pointer type, while poly_any is a nullable value type.

    (Please treat poly_any as pseudocode - I have not tested this specific implementation of it. It is also significantly more advanced than what you are doing, so just use the unique_ptr solution. The point of poly_any is that we can regain value semantics while still supporting polymorphism in C++ with a bit of help from a library.)