Runtime polymorphism without dynamic memory allocation

Another one on polymorphism

This time is about not using heap allocation while having runtime polymorphism. I will use std::variant for this, so nothing new. What got my attention is how the polymorphic objects are used if stored in a variant. This is the main topic of this short article.

The use case is a factory function that creates polymorphic objects.

Virtual inheritance

I’m taking it step by step, starting with the classic approach using virtual inheritance. For this, I need some pointers, of course.

#include <cassert>
#include <memory>

struct P {
    virtual int f(int) const = 0;
    virtual ~P() = default;
};

struct A : P {
    int f(int in) const override {return in + 1;}
};

struct B : P {
    int f(int in) const override {return in + 2;}
};

std::unique_ptr<P> factory(char o) {
    switch(o) {
        case 'a': return std::make_unique<A>();
        default: return std::make_unique<B>();
    }
}

int main() {
    assert(factory('a')->f(1) == 2);
    assert(factory('b')->f(1) == 3);
}

std::variant

The std::variant solution is to have a variant with all the possible types instead of pointers to the types. This will avoid heap allocations. And it will break the need for inheritance, having objects that are not coupled to a base class anymore.

#include <cassert>
#include <variant>

struct A {
    int f(int in) const {return in + 1;}
};

struct B {
    int f(int in) const {return in + 2;}
};

std::variant<A, B> factory(char o) {
    switch(o) {
        case 'a': return A{};
        default: return B{};
    }
}

int main() {
    assert(std::visit([](auto&& o){ return o.f(1); }, factory('a')) == 2);
    assert(std::visit([](auto&& o){ return o.f(1); }, factory('b')) == 3);
}

 

What bothers me in this approach is that the caller has the responsibility to write a callback and visit the variant. I exposed the object storage (variant) and required the caller to use its API. The previous pointer approach is more simple and… natural, I would say

factory('a')->f(1)

while with variant feels like too much to ask from the caller

std::visit([](auto&& o){ return o.f(1); }, factory('a'))

std::variant, the abstract approach

I want to abstract the visit need. Instead of returning the variant from the factory, I’ll return a lambda. The caller will need to just call a function.

factory('a')(1)

I am studying only the design. Some extra copies can occur and this can have a performance penalty if your objects are not easy to copy or not movable.

#include <cassert>
#include <variant>

struct A {
    int f(int in) const {return in + 1;}
};

struct B {
    int f(int in) const {return in + 2;}
};

auto factory(char o) {
    std::variant<A, B> object;

    switch(o) {
        case 'a': object = A{}; break;
        default: object = B{}; break;
    }

    return [object](int in){
        return std::visit([in](auto&& obj){ return obj.f(in); }, object);
    };
}

int main() {
    assert(factory('a')(1) == 2);
    assert(factory('b')(1) == 3);
}

 

I didn’t write any benchmarks nor compared the generated assembly in a complex context. The std::variant examples above are too simple and everything is known at compile-time.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.