Posts Polymorphic Usage Of Non - Polymorphic Class
Post
Cancel

Polymorphic Usage Of Non - Polymorphic Class

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!

Trending Tags