The C++ logo, by Jeremy Kratz, licensed under CC0 1.0 Universal Public Domain Dedication

Generic owning type erasure

This is a follow-up of Owning type erasure for simple types, type erased view types, and custom virtual tables (they should all be available under the keyword type erasure)

Until now, all methods presented tried to avoid allocating memory (in that sense, they were lightweight).

In the case of custom virtual tables, the main role of the type-erased class was just a wrapper around some function pointers.

For type erased view types, the type-erased class mainly consists of a custom virtual table and a pointer to the actual object.

Owning type erasure for simple types, contrary to the previous approaches, owns the object, but only up to a certain size determined at compile time. The implementation was also restricted to "simple" types, in order to exploit std::memcpy to manage the lifetime of objects for us. The presented class can be extended to use placement new for creating objects in place in the buffer, you must also remember to call the destructor manually.

Even if works™, the implementation has a lot of gotchas and is hard to write correctly.

In this note, the described technique is considered heavyweight, as it allocates memory, but it is more robust, as in we do not need to bypass the type system at all (there are no explicit casts)

Class hierarchies

The "easy" way (and for many "classic", or even "only") to provide type erasure is to make a class hierarchy.

Supposing you want to model a widget, you might end with something like

struct monitor;

struct widget0{
    // some virtual methods with a default implementation
    virtual bool enabled();
    virtual void enable(bool b);
    virtual ~widget0() = default;
    // some pure virtual methods
    virtual int set_height(int) = 0;
    virtual int get_height() = 0;
    virtual int set_widtht(int) = 0;
    virtual int get_width() = 0;
    virtual void draw(monitor*) = 0;
};

struct window: widget0 {
    // ...
};

struct button: widget0 {
    // ...
};

struct menu: widget0 {
    // ...
};
struct text: widget0 {
    // ...
};

and the resulting code will need to pass around widget0*.

One of the drawbacks of this method is that it is not possible to use an int for representing a number (assuming that it makes sense), or a class from another library that could work as a widget.

One needs to define a wrapper class that inherits from widget0, and reimplement all required methods.

A possible way to define an owning type-erased class is to hide the class hierarchy as an implementation detail.

A type-erased widget class

#include <cassert>
#include <memory>

struct monitor;

class widget1 {
  public:

    template <typename T>
    explicit widget1(T x) : self(std::make_unique<impl<T>>(std::move(x))){
    }

    widget1(const widget1& o) : self(o.self->clone()){
    }
    widget1& operator=(const widget1& o){
        if(this != &o){
            self =o.self->clone();
        }
        return *this;
    }

    // begin interface to support
    void draw(monitor* m) {
         assert(m != nullptr);
         self->draw_internal(m);
    }
    // end interface to support

  private:
    struct interface {
        interface() noexcept = default;
        virtual ~interface() = default;

        virtual std::unique_ptr<interface> clone() const = 0;

        // begin interface to support
        virtual void draw_internal(monitor* m) = 0;
        // end interface to support
    };
    template <typename T>
    struct impl final: widget1::interface {
        impl(T x) : data(std::move(x)) { }

        std::unique_ptr<interface> clone() const override
        {
            return std::make_unique<impl>(*this);
        }
        T data;

        // begin interface to support
        void draw_internal(monitor* m) override
        {
            data.draw(m);
        }
        // end interface to support
    };

    std::unique_ptr<interface> self;
};

For brevity, the widget1 class only supports the draw method.

Contrary to the previous implementation, widget1 itself has no virtual functions.

The implementation forwards all parameters to the self member variable, which contains one instance pointing to widget1::interface.

Note 📝
One advantage of this technique is that validations can be done once in the non-virtual method. In fact, virtual methods should probably not be public.

How can this class be used?

here is a minimal example:

#include <cassert>
#include <cstdio>
#include <memory>
#include <string>
#include <vector>

struct monitor;

class widget {
  public:

    template <typename T>
    explicit widget(T x) : self(std::make_unique<impl<T>>(std::move(x))){
    }

    widget(const widget& o) : self(o.self->clone()){
    }
    widget& operator=(const widget& o){
        if(this != &o){
            self = o.self->clone();
        }
        return *this;
    }

    // begin interface to support
    void draw(monitor* m) {
         assert(m != nullptr);
         self->draw_internal(m);
    }
    // end interface to support

  private:
    struct interface {
        interface() noexcept = default;
        virtual ~interface() = default;

        virtual std::unique_ptr<interface> clone() const = 0;

        // begin interface to support
        virtual void draw_internal(monitor* m) = 0;
        // end interface to support
    };
    template <typename T>
    struct impl final: widget::interface {
        impl(T x) : data(std::move(x)) { }

        std::unique_ptr<interface> clone() const override
        {
            return std::make_unique<impl>(*this);
        }
        T data;

        // begin interface to support
        void draw_internal(monitor* m) override
        {
            data.draw(m);
        }
        // end interface to support
    };

    std::unique_ptr<interface> self;
};

struct monitor{
    int indent = 0;
    void draw_pixels(const char* text){
        printf("%*s %s\n", indent*2, "", text);
    }
};

// some classes that previously were subclasses

struct button{
    void draw(monitor* m, int i = 0) const {
        m->draw_pixels("button");
    }
};

struct menu{
    void draw(monitor* m){
        m->draw_pixels("menu");
    }
};

struct buttons{
    std::vector<button> v;
    void draw(monitor* m){
        m->draw_pixels("buttons");
        ++m->indent;
        for(auto& e : v){
            e.draw(m);
        }
        --m->indent;
    }
};

struct pane{
    std::vector<widget> v;
    void draw(monitor* m){
        m->draw_pixels("pane");
        ++m->indent;
        for(auto& e : v){
            e.draw(m);
        }
        --m->indent;
    }
};

int main(){
    monitor m;
    {
        auto w = widget(button());
        std::puts("= draw button");
        w.draw(&m);
        std::puts("= draw menu");
        w = widget(menu());
        w.draw(&m);
    }
    {
        std::puts("= draw buttons");
        buttons vb{{button(), button()}};
        vb.draw(&m);
    }

    {
        std::puts("= draw empty pane");
        pane p;
        p.draw(&m);

        p.v.emplace_back(button());
        p.v.emplace_back(menu());
        std::puts("= draw non-empty pane");
        p.draw(&m);
        std::puts("= draw non-empty pane (2)");
        p.v.emplace_back(p);
        p.draw(&m);
    }
}

The output of this program should look similar too:

= draw button
 button
= draw menu
 menu
= draw buttons
 buttons
   button
   button
= draw empty pane
 pane
= draw non-empty pane
 pane
   button
   menu
= draw non-empty pane (2)
 pane
   button
   menu
   pane
     button
     menu

As mentioned earlier, a pointer to a class hierarchy is an implementation detail.

In fact, in main there are no pointers, except for &m.

The second interesting part of this example is the line p.v.emplace_back(p);, which produces the following text when printed out

pane
  button
  menu
  pane
    button
    menu

As the type is polymorphic and mostly has value semantic, it is not an issue for std::vector to be able to hold a widget1 that is type-erasing another vector of widgets.

Before analyzing the differences between this type-erased class and a class hierarchy, let’s address the following issue.

It is still not possible to type-erase an int, as it does not have any member method, or std::string, as it does not have the member methods required by the widget interface, and by both types, we cannot extend them.

Difference between the two approaches

Users of the widget do not need to deal with pointers

In the case of a widget0, the end-user often must use pointers, for example, if a structure needs to hold a widget, it might look like

struct class_holding_widget{
    std::unique_ptr<widget0> ptr;
};

On the contrary, with widget1, the equivalent code would look like

struct class_holding_widget{
    widget1 w;
};

The type-erased class has (mostly) value semantic

As already mentioned, widget1 has mostly value semantic, while widget0 does not, although it might provide a clone or copy method.

Why only mostly?

Because if the type that is type-erased does shallow copies (like a pointer or reference type), then also the copy-constructor of widget1 will do shallow copies for some instances.

Note that it is possible, for example with a static_assert, to avoid creating an instance from a pointer, but it is generally not possible to detect if a class has a shallow or deep copy-constructor.

Note that being able to copy and move widget1 is not a requirement. It is perfectly fine to delete the copy constructor.

Less coupling between functions

In the case of widget0, functions are tightly coupled, which has some minor disadvantages.

Consider

#include <memory>
#include <cstdio>

struct widget0{
    virtual bool enabled(){std::puts("widget0::enabled"); return true;}
    virtual ~widget0() = default;
};

struct window: widget0 {
    bool enabled(int i = 0) {std::puts("window::enabled"); return true;}
};

struct button: widget0 {
    bool enabled() const {std::puts("button::enabled"); return true;}
};


int main(){
    auto ptr1 =std::unique_ptr<widget0>(std::make_unique<window>());
    ptr1->enabled(); // calls widget0::enabled, and not window::enabled

    auto ptr2 =std::unique_ptr<widget0>(std::make_unique<button>());
    ptr2->enabled(); // calls widget0::enabled, and not button::enabled
}

Both in the case of window and button, the function signature differs, and thus enabled has not been overridden.

With virtual it is possible to avoid this issue, but what if we wanted to have a subclass with the method marked as const?

With the widget1, this is not an issue, as long as the call data.enabled(); is valid. It can be const-qualified, it can have additional defaulted parameters, it can override another function, or even take a parameter that is convertible to another one.

Such flexibility might not be always desired, and if problematic, it is possible to destructure a function, and use static_assert to enforce some properties.

In the case of class hierarchies, at least if the member function were defined as pure (thus with = 0), then we would get a compilation error (even without writing override) instead of the undesired behavior at runtime.

Also, note that contrary to a class hierarchy, with some metaprogramming, it is possible to reuse even more code.

For example, we can support classes that use a different function name, or a free function instead of a member function, with the techniques shown here:

    struct interface {
        interface() noexcept = default;
        virtual ~interface() = default;

        virtual std::unique_ptr<interface> clone() const = 0;

        // begin interface to support
        virtual void draw_internal(monitor* m) = 0;
        virtual bool enabled_internal(monitor* m) = 0;
        // end interface to support
    };
    template <typename T>
    struct impl final: widget1::interface {
        impl(T x) : data(std::move(x)) { }

        std::unique_ptr<interface> clone() const override
        {
            return std::make_unique<impl>(*this);
        }
        T data;

        // begin interface to support
        void draw_internal(monitor* m) override
        {
            if constexpr(requires{ &T::draw; }){
                data.draw(m);
            } else {
                // if no member function, assume there is a free function
                draw(data, m);
            }
        }
        bool enabled_internal(monitor* m) override
        {
            if constexpr(requires{ &T::enabled; }){
                return data.enabled();
            } else if constexpr(requires{ enabled(std::declval<T>()); }){
                return enabled(data)
            } else { // if no function, it is always enabled
                return true;
            }
        }
        // end interface to support
    };

Classes that can be used as widget1, do not need to have widget1 as a dependency

While it might not make a difference in some projects, it might be extremely relevant for others.

Fewer dependencies between components mean that there are more chances to parallelize work during compilation, thus better compile times.

It means that classes of different libraries can be used as a widget1, without them having an underlying common library.

One side-effect is that it helps to avoid circular dependencies too.

No explicit casts are required

Contrary to most other techniques shown for type-erasure, no method uses any explicit cast.

Both widget0 and widget1 do not require explicit casts.

Every new instance requires an allocation

The constructor of widget1 always allocates, as it calls std::make_unique<impl<T>>:

    template <typename T>
    explicit widget1(T x) : self(std::make_unique<impl<T>>(std::move(x))){
    }

While with widget0 it is common, but not required to use the heap too, the current implementation of widget1 makes it a hard requirement.

A possible workaround would be to to implement a "small object optimization", like std::any already does.

At compile time, the constructor of widget1 needs to test for size and alignment of T, and if adequate, create a new instance in a buffer instead of calling std::make_unique<impl<T>>.

While this approach will lead to a more performant implementation, it will also greatly increase the complexity of the widget1 class.

Also, this approach does not "fix" the issue entirely. The user of widget1 is not able to decide that certain objects should not be allocated. Some users might even prefer to have a compile error instead of an unexpected allocation.

Another approach would be to define ref_for_widget1, a non-owning type-erasing class, whose sole existence is for an additional constructor of widget1, that would avoid the allocation:

    explicit widget1(ref_for_widget1 x);

Internally, widgets would have either an unique_ptr<widget1::interface>, or a ref_for_widget1.

While it is not nice, as an instance of widget1 with a ref_for_widget1 would not have value semantic, it was also already true before, as it is not possible to verify that some classes do not make mutable shallow copies.

With ref_for_widget1, this misfeature would at least be easier to acknowledge when creating such objects.

Might cause more copies than necessary

It might not be a fair critique, after all, this is an owning type-erased class. But since I’m comparing it to a class hierarchy, I was asked if one should be concerned.

The constructor of widget1 invokes the move or copy constructor of objects, and the copy-constructor of widget1 also makes copies.

With a public virtual class hierarchy, since objects normally do not have value semantics, there are little to no copies.

Some copies can be avoided by defining an efficient move constructor, and at that point, the author of the widget class needs to decide what happens if someone tries to use a "moved-from" widget.

The main possibilities are

  • throw an exception (or some other form of recoverable error)

  • a non-recoverable error like std::exit, std::abort, std::quick_exit, std::terminate, …​

  • define some "default" behavior for every method

  • let it be UB (in this example it would be in practice to dereference nullptr, I recommend to add an assert)

Also ref_for_widget1 helps to avoid costly copies, but then, the widget class would not really be an owning class, would it?

Note that the same considerations hold for std::string versus const char* (and std::string_view), and yet, in many cases std::string is, if mutable and independent strings are required, less error-prone and more performant than const char*.

A possible approach for avoiding copies is if all operations are const: shallow copies. More on that in another section.

Boilerplate code

This is the biggest disadvantage of the proposed type-erased class.

We need to define the same function multiple times: for widget1, widget1::interface, and widget1::impl<T>.

It might not seem problematic for a class that declares two functions, but what if we have 30 or 40? qwidget declares a lot of functions, imagine yourself typing the same function three times. It is extremely error-prone and verbose for no good reason.

Note 📝
Also for a non-owning type-erased class we need to define every function more than once.

I am not aware of an "elegant" solution for the boilerplate code, I do not see how a macro can help; probably an external code generator could avoid some duplication.

At least such duplication is necessary only inside the widget class, and not outside of it.

It is possible to use a fundamental type as widget1

Since widget1::impl can use free functions or fall back to some predefined behavior, any type can be used as widget1, even an int.

Support for dynamic_cast

dynamic_cast will not work on widget1, as there is no class hierarchy.

If such an operation is needed, then one needs to provide an accessor to the underlying data, similarly to this accessor method

Default methods

In the case of a class hierarchy, a subclass can reuse the implementation of a parent class without writing a single line of code, or deciding to override the function:

#include <memory>
#include <cstdio>

struct widget0{
    virtual bool enabled(){std::puts("widget0::enabled"); return true;}
    virtual ~widget0() = default;
};

struct window: widget0 {
    // the implementation of widget0::enabled already does the Right Thing(TM)
};

struct button: widget0 {
    // button is special, it needs to override the behavior of enabled
    bool enabled() override {std::puts("button::enabled"); return true;}
};

There is no equivalent mechanism for widget1 backed directly in the language, but there are several techniques to achieve something similar.

Member functions

template <typename D>
struct default_enabled {
    friend D;
    bool enabled(){std::puts("widget1::enabled"); return true;}
    void enable(bool b) { /* ... */ }
};

// window is normal, it can reuse a common implementation
struct window: default_enabled<window>{
    // other methods
};

// window2 is normal, it can reuse a subset of a common implementation
struct window2: private default_enabled<window>{
    // other methods
    using default_enabled<window>::enabled;
    void enable(bool b) { /* ... */ }
};

// button is special, it needs its own enabled
struct button {
    bool enabled() override {std::puts("button::enabled"); return true;}
    void enable(bool b) { /* ... */ }
};

Unfortunately, this method changes the layout of a class.

Free functions

With free functions, it is possible to write a catch-all function

template <class T>
bool enabled(T&){
    std::puts("widget1::enabled"); return true;
}

// window is normal, it can reuse a common implementation
struct window{
};

// button is special, it needs its own enabled
struct button {
};
bool enabled(button&){
    std::puts("button::enabled"); return true;
}

If a catch-all function is too generic (maybe because the function name is too generic), it is still possible to restrict it to fewer types.

A custom namespace to use as a customization point (which also works with friend functions, but is problematic for hidden friends), concepts, or define a common type that other types can convert to it (it’s type-erasure all the way down…​), for example:

struct widget1;
struct window;

struct common{
    common(const widget1&){}
    common(const window&){}
    // eventually other types
};
bool enabled(common){
    std::puts("widget1::enabled"); return true;
}

// window is normal, it can reuse a common implementation
struct window{
};

// button is special, it needs its own enabled
struct button {
};
bool enabled(button&){
    std::puts("button::enabled"); return true;
}

Avoid unnecessary copies if everything is immutable

If all operations supported by widget do not change the data, then there is little need to create copies.

In this case, replacing unique_ptr with shared_ptr and removing the copy-constructor and clone method is all that is needed:

#include <cassert>
#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct monitor;

class widget2 {
  public:

    template <typename T>
    explicit widget2(T x) : self(std::make_shared<impl<T>>(std::move(x))){
    }

    // begin interface to support
    friend void draw(const widget2& w, monitor* m) {
         assert(m != nullptr);
         w.self->draw_internal(m);
    }
    // end interface to support

  private:
    struct interface {
        interface() noexcept = default;
        virtual ~interface() = default;

        // begin interface to support
        virtual void draw_internal(monitor* m) const = 0;
        // end interface to support
    };
    template <typename T>
    struct impl final: widget2::interface {
        impl(T x) : data(std::move(x)) { }

        T data;

        // begin interface to support
        void draw_internal(monitor* m) const override
        {
            draw(data, m);
        }
        // end interface to support
    };

    std::shared_ptr<interface> self;
};

struct monitor{
    int indent = 0;
    void draw_pixels(std::string_view text){
        std::cout << std::string(indent*2, ' ') << text << "\n";
    }
};

// some classes that previously were subclasses

struct button{
};
void draw(const button&, monitor* m) {
    m->draw_pixels("button");
}

struct menu{
};
void draw(const menu&, monitor* m){
    m->draw_pixels("menu");
}
struct buttons{
    std::vector<button> v;
};
void draw(const buttons& bt, monitor* m){
    m->draw_pixels("buttons");
    ++m->indent;
    for(auto& e : bt.v){
        draw(e, m);
    }
    --m->indent;
}

struct pane{
    std::vector<widget2> v;
};
void draw(const pane& p, monitor* m){
    m->draw_pixels("pane");
    ++m->indent;
    for(auto& e : p.v){
        draw(e, m);
    }
    --m->indent;
}

void draw(int i, monitor* m){
    m->draw_pixels("int (" + std::to_string(i) + ")");
}

int main(){
    monitor m;
    {
        auto w = widget2(button());
        std::cout << "= draw button\n";
        draw(w, &m);
        std::cout << "= draw menu\n";
        w = widget2(menu());
        draw(w, &m);
        std::cout << "= draw int\n";
        w = widget2(-1);
        draw(w, &m);
    }
    {
        std::cout << "= draw buttons\n";
        buttons vb{{button(), button()}};
        draw(vb, &m);
    }

    {
        std::cout << "= draw empty pane\n";
        pane p;
        draw(p, &m);

        p.v.push_back(widget2(button()));
        p.v.push_back(widget2(menu()));
        std::cout << "= draw non-empty pane\n";
        draw(p, &m);
        std::cout << "= draw non-empty pane (2)\n";
        p.v.push_back(widget2(p));
        draw(p, &m);
    }
}

The output still looks like

= draw button
button
= draw menu
menu
= draw int
int (-1)
= draw buttons
buttons
  button
  button
= draw empty pane
pane
= draw non-empty pane
pane
  button
  menu
= draw non-empty pane (2)
pane
  button
  menu
  pane
    button
    menu

Note that since widget2 does create a copy during construction, it does if some types have mutating methods, like std::string::clear.

The constructor of widget2 creates a copy internally that is normally not reachable, thus it is generally sufficient to look at the interface of widget2 to decide if a shallow copy is fine or not.

There are ways to access the internal copy, but not by accident. For example, a user-defined class could reach the internal copy by storing a reference in a global variable, which is another reason why global mutable variables should be used with great care.

Similar to the issue with value semantic and shallow copies, it is not possible to detect if the type-erased types are really immutable, or have shallow constness.

This issue is not unique to widget2 or type-erased types, but it is important to realize what are the limitations of certain design decisions, what the consequences are, and how to react.

For example, if a type-erased type would not be immutable, would it mean to remove shallow copies? It could do more damage than good.

On the other hand, it would be preferable to make all types deep-const.

Conclusion

Without some "small buffer optimizations", there might be allocation when there were none before. With a "small buffer optimization" it can be possible to greatly improve performance compared to handling pointers manually, just like std::string is generally more performant than most string-like classes in the wild.

Not using virtual inheritance as the API of a class might seem like a novel method, but it is not.

Not using public virtual functions makes it easier to establish invariants and value semantics makes it easier to reason about the state of a program.

If your classes are not copyable, or they do not have the equivalent of a clone method, then an equivalent implementation would be using shallow clones with a mutable interface: generally something it is desirable to avoid.

Contrary to some other techniques, there are no casts (without a "small buffer optimization"), thus the implementation is robust, and most if not all possible implementation errors are caught at compile time.

One minor disadvantage of this technique is that there is no definitive answer on how to support move semantics, and the major disadvantage is the amount of boilerplate code.

Granted, through some metaprogramming techniques it is possible to reduce the amount of code for the classes implementing the API of the widget, but for the widget class itself, there no way is in C++ to avoid writing the same function signature multiple times.


Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me anytime.