Some people may be mixing up following two terms: inheritance and polymorphism. And, while polymorphism, at least dynamic one, depends on inheritance, inheritance itself is a standalone feature of a specific programming language. One may define a class that’s inheriting another class but is not suitable for polymorphic usage. How could this happen? How can we get around this and make that class suitable for polymorphism after all? Answers to these questions you’ll hear in the next few minutes.
What Is Polymorphism After All?
Dynamic polymorphism, at least in C++, is possible only when there is an indirection in the code, or when there is a pointer or reference to the specific type. To get a better understanding of what I have said, let’s imagine a following situation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Base
{
virtual ~Base() {}
virtual void foo()
{
std::cout << "Base::foo\n";
}
};
struct Derived : Base
{
void foo() override
{
std::cout << "Derived::foo\n";
}
};
Now, even though our Derived
class inherits from another class, creating an object on stack like:
1
2
Derived x;
x.foo(); // output: Derived::foo
is not considered as polymorphism because there is no indirection. It is simple inheritance, no more, no less.
But… If we create an indirection, in this case by using a new
operator, like in the below snippet, then this is indeed a polymorphism:
1
2
3
Base * x = new Derived();
x->foo(); // output: Derived::foo
delete x;
Now that we have mastered basic OOP principles, let’s get to the main point…
Class Designed For Inheritance Only
What if our Derived
class is designed only for the inheritance purpose… Its designer didn’t want or maybe didn’t care about class’ users that need to create indirections in their code. An example of such classes is below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base
{
public:
virtual void foo()
{
std::cout << "Base::foo\n";
}
protected:
~Base() {}
};
struct Derived : Base
{
void foo() override
{
std::cout << "Derived::foo\n";
}
};
If we ever want to create e.g. std::vector
of Derived
objects, we would do so as simple as:
1
2
3
4
5
std::vector< Derived > v{ 5 };
for ( auto && d : v )
{
d.foo();
}
and compilation would end up with success (check out the demo). So there is no problem here, right?
Well, there is actually a possible trap in the above code. You may have noticed I omitted the virtual destructor from the Base
class’ definition. The final
keyword is also missing from Derived
class’ definition. In conjunction with the fact that the std::vector
class creates an indirection (by using new
and delete
) which is possible to be polymorphic, this omittance of the final
keyword and virtual destructor results in compiler’s ignorance of whether indirection points to the object of Derived
class or the object of a derived class of the Derived
. So, theoretically, there is a possibility of ending up into Undefined Behavior (like in this example here).
Here is the explanation from the standard:
In a single-object delete expression, if the static type of the object to be deleted is different from its dynamic type and the selected deallocation function (see below) is not a destroying operator delete, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined. In an array delete expression, if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.
If you enable -Wdelete-non-virtual-dtor
compiler flag, you can see what I am talking about (check out the demo).
It’s an obvious fix to make our Derived
class final or to add virtual destructor to our Base
class as stated by CppCoreGuidelines:
A base class destructor should be either public and virtual, or protected and non-virtual.
But, what if we have no power over Derived
class nor Base
class… we cannot change it… it’s simply there and we need to live with it. Let’s see how we can get around this…
Workaround To Force Non - Polymorphic Behavior
Our goal is now to be able to use Derived
class in the std::vector
but without changing the layout of Derived
or Base
class. Those are part of the 3rd party library. The only thing we need to get rid off at this point is - possibility of polymorphism or, in other words, indirection.
There is a simple workaround to make this working - wrap up the Derived
class and have std::vector
of that Wrapper
class like (check out the demo):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Wrapper
{
Derived x;
constexpr Derived * operator->() noexcept { return &x; }
constexpr Derived const * operator->() const noexcept { return &x; }
constexpr Derived & operator*() noexcept { return x; }
constexpr Derived const & operator*() const noexcept { return x; }
};
int main()
{
std::vector< Wrapper > v{ 5 };
for ( auto && d : v )
{
d->foo();
}
return 0;
}
As you can see, we have overloaded some operators to make accessing the instance of Derived
class more user friendly. And the above solution works! But why does it work? Because there is no polymorphism / indirection in the above code. Our x member is not a pointer to Derived
nor reference to Derived
object, so there is no danger of it pointing out to an object of a derived class of the Derived
class. Now, it’s a simple self-conscious little class data member.
EDIT: In addition to above example, there is even shorter workaround:
1
struct Wrapper final : Derived {};
Above Wrapper
structure marked as final
is enough to silence the warning and is much more neat than overloading ->
and *
operators.
Conclusion
Throughout this blog post, I have talked about programming aspects that one part of you may find obvious while another part may not be familiar with. Anyway, this situation is quite possible to happen in everyday life and I hope a simple solution, as this, will help you.
Have you ever ran into this kind of problem? How have you solved it? What do you think of this solution? Any other questions and thoughts as well as answers to questions I mentioned now please put in the comment section below… and see you till next blog post!