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.
b
lost when returning?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).
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.)