-
Notifications
You must be signed in to change notification settings - Fork 24
An alternative implementation with a small buffer optimisation (SBO) #124
Description
I made an implementation of a polymorphic_value with SBO. It has a very similar API as this incarnation but removes the possibility to take over ownership of an object already existing on the heap. This was primarily due to the observation that if this object is small enough to fit the SBO it would have to be copied anyway, but it also solved the slicing problem that could so deviously occur in the original design. Instead the contained object is always created directly inside the polymorphic_value, either using an in_place_type constructor, a special emplace method or a make_polymorphic_value function.
I also opted to remove all possibility to assign, copy or cast between polymorphic_value<T> and polymorphic_value<U> to avoid the need for linked lists of this pointer conversion functions. This allows my implementation to use placement new on a member which allows polymorphic_value to work entirely without heap allocations unless the contained object is larger than the SBO buffer size. The performance is also improved as accessing the object only requires one virtual call, no access to control block object(s) on the heap.
Removing the constructors from pre-existing heap object pointers makes polymorphic_value more similar to a regular by value object, but I still opted to have a default constructor that leaves the object empty, an operator bool() to test this condition and a reset() to destroy the contained object. This is for two reasons. One is that it is often convenient to be able to encode an empty state, for instance if the required contained type of a member is not known when the surrounding object is constructed. The other reason is that if the constructor of the contained object throws in the in_place_type constructor the polymorphic_value would still be empty even if no default constructor was provided. std::variant has a special valueless_by_exception state for this situation and suggests adding a std::monostate as the first alternative if the variant is allowed to be empty. But as monostate does not inherit the T in a polymorphic_value this is not a feasible way to create a "nullable" polymorphic_value, so it seems best to keep this aspect of pointerness.
An alternative would be to specialize std::optional for std::polymorphic_value so that a plain polymorphic_value only has valueless_by_exception but an optional<polymorphic_value<T>> has optional::operator bool() implemented without overhead using the handler mechanism in polymorphic_value (via a friend declaration). The drawback with this solution is mainly that operator*() of optional would return the polymorphic_value and another * would have to be applied to get a reference to the contained object.
A third alternative would be to call the entire thing polymorphic_optional or possibly provide both polymorphic_value and polymorphic_optional.
A fourth alternative would be to retain the specialization std::optional<std::polymorphic_value<T>> but adapt the API to become logical in the specialization of std::optional, i.e. let operator* do both steps etc.
My preference would be to just document that std::polymorphic_value has the optional-ness built in and this would basically entail changing the name of get() to value() and let it throw on empty. The monadic operations of C++23 could also easily be added. The drawback with this is that dynamic_casting when the polymorphic_value is empty gets complicated. With get() you can write dynamic_cast<U*>(v.get()) to see if v contains an instance of U while with value you'd have to check for NULL first, then call value and take the address of the reference before doing the dynamic_cast. A U* get<U>() method could be added to simplify this, while a U& value<U>() would throw if the contained value is not a U (or empty).
Here are the files for my polymorphic_value and a simple test/demo program. I don't really know how to handle this "extra code base", maybe I should clone the repo and add a subdirectory? Note that this version still has more of a smart pointer API than an optional API.