We’re back on type erasure today, with a simple polymorphic implementation. C++26 is going to introduce two new types:
std::polymorphic: a wrapper above a polymorphic dynamically allocated object with value-like semanticsstd::indirect: a wrapper above a dynamically allocated object, also with value-like semantics
The only drawback of std::polymorphic It relies on an interface with virtual functions. But thanks to that, you continue to manipulate value, with its copy or move :).
std::polymorphic usage
The goal of this article is to write the code that allows us to write:
struct Drawable {
// No need for real virtual destructor
virtual void draw(std::ostream &stream) const = 0;
};
struct Circle final : Drawable {
Circle(int radius) : radius{radius} {}
void draw(std::ostream &stream) const override {
stream << "Draw circle of " << radius << " radius" << std::endl;
}
int radius;
};
struct Rectangle final : Drawable {
Rectangle(int width, int height) : width{width}, height(height) {}
void draw(std::ostream &stream) const override {
stream << "Draw rectangle of (" << width << "," << height << ")" << std::endl;
}
int width, height;
};
int main() {
polymorphic<Drawable> circle1 = Circle{10};
polymorphic<Drawable> circle2 = circle1;
polymorphic<Drawable> rectangle1 = Rectangle{800, 600};
polymorphic<Drawable> rectangle_valueless = Rectangle{800, 600};
polymorphic<Drawable> rectangle2 = std::move(rectangle_valueless);
circle1->draw(std::cout);
circle2->draw(std::cout);
rectangle1->draw(std::cout);
rectangle2->draw(std::cout);
std::cout << std::boolalpha << rectangle_valueless.valueless_after_move() << std::endl;
}
As you can see, we can copy the objects, leading to a true copy; we can move the objects, leading the object to be in valueless_after_move state. Here is the godbolt link if you want it.
std::polymorphic implementation
We are going to implement our own simple std::polymorphic. To do so, we are going to do it in this order:
- The structure
- The constructors
operator=- The accessors
- The
valueless_after_movefunction.
The structure
As you can see, our polymorphic implementation takes the interface as a template parameter. Since we talked about type erasure, we can also follow the same approach as usual
- A polymorphic
Conceptstructure - A templated
Model<T>structure which inherits fromConcept. - A
std::unique_ptr<Concept>which manages the erased type.
template<typename Interface>
class polymorphic {
struct Concept {
virtual ~Concept() = default;
};
template<typename Object>
struct Model : Concept {
Model(Object value) : m_value{std::move(value)}{}
private:
Object m_value;
};
std::unique_ptr<Concept> m_object;
};
Which kind of behavior do we need to implement?
- We need to access the underlying type through
Interface. We will expose aget_addressfunction returning aInterface*. - We need to be able to copy the object. We will then expose a
clonefunction.
It looks like that:
struct Concept {
virtual ~Concept() = default;
virtual Interface* get_address() = 0;
virtual std::unique_ptr<Concept> clone() const = 0;
};
template<typename Object>
struct Model : Concept {
Model(Object value) : m_value{std::move(value)}{}
Interface* get_address() override {
return std::addressof(m_value);
}
std::unique_ptr<Concept> clone() const override {
return std::make_unique<Model<Object>>(m_value);
}
private:
Object m_value;
};
Now that we have the basics, let’s handle the different constructors.
Constructors
We have a different kind of constructor:
- Constructor with the value as an argument
- Copy constructor
- Move constructor
As we can see, in our implementation, I didn’t put a default constructor. Then, except if we move the object, our polymorphic will never be null.
The implementation is straightforward; we call the clone() function for the copy constructor and make_unique<Model<T>> for the main constructor:
template<typename Object>
polymorphic(Object value) noexcept
: m_object{std::make_unique<Model<Object>>(std::move(value))} {
static_assert(std::is_base_of_v<Interface, Object>);
}
polymorphic(polymorphic&&) noexcept = default;
polymorphic(const polymorphic &object)
: m_object{object.m_object->clone()}{}
As we can see, the implementation is straightforward. The move constructor calls the move constructor from unique_ptr and that’s over.
operator=
The operator= functions are the same:
polymorphic &operator=(polymorphic&&) noexcept = default;
polymorphic& operator=(const polymorphic& object) {
m_object = object->clone();
return *this;
}
There is nothing to say here
Accessors functions
The accessor functions are also straightforward; we need to call the get_address functions.
const Interface &operator*() const {
return *m_object->get_address();
}
Interface& operator*() {
return *m_object->get_address();
}
const Interface *operator->() const {
return m_object->get_address();
}
Interface *operator->() {
return m_object->get_address();
}
valueless_after_move
Another straightforward implementation:
bool valueless_after_move() const {
return m_object == nullptr;
}
Conclusion
Our implementation is finished, but there are some things to do to improve it:
- Adding more constructors, like
in_placeones. - Manage custom allocator
- Adding small object optimization
We can also enhance the safety of the object by checking with the std::is_final_v trait. If the object is final, to be sure we are not losing some information, and propose eventually a tag not_final to allow the user to use types that are not flagged as final.
It was a short article, but I hope you liked it.
Leave a Reply