This is the summary page for jv::BoundedPoly.
To see the reference, go there: reference.html.
Abstract
BoundedPoly<Storage, Base, Mover = …> is a type abstractor.
It stores any no-throw movable polymorphic derived class of Base that fits into size and alignment of Storage.
It provides semantics similar to std::unique_ptr<Base> and std::variant<…>:
-
The memory is owned by the instance (like both, and unlike
std::reference_wrapper<Base>). -
The type can change over time (like both).
-
The actual type of the stored value is not known (like
unique_ptr). -
The set of accepted types is not closed (line
unique_ptrand unlinevariant). -
It is allocated on the stack (like
variant). -
It cannot be empty, even when exceptions are thrown (like
variant, but greater guarantee).
This allows to use both polymorphic-semantics and stack-allocation (and so, cache-friendlyness).
Examples
Shape
struct IShape {
virtual ~IShape() noexcept {}
virtual auto get_area() const noexcept -> float = 0;
virtual void move_to(void*) && noexcept = 0;
};
We first create an interface named IShape.
It provides a get_area() method which returns the area of the shape.
It also provides a move_to method to move itself to an uninitialized storage.
constexpr std::size_t ShapeMaxSize = 16;
We choose an arbitrary size to store shapes.
using Shape = jv::BoundedPolyVM<std::aligned_storage_t<ShapeMaxSize>, IShape,
&IShape::move_to>;
We define Shape as an alias of BoundedPolyVM<Storage, Base, Method>, which is an alias to BoundedPoly<Storage,Base,VirtualMover<Base, Method>>.
VM (Virtual Mover) means that we used a virtual method in order to make move operations.
So BoundedPoy<Storage,Base,Mover> is instantiated with the following template parameters:
-
Storage = std::aligned_storage_t<ShapeMaxSize>: it can only stores shapes withsizeof(_) <= ShapeMaxSize. -
Base = IShape: the polymorphic type abstracted -
Mover = VirtualMover<IShape, &IShape::move_to>: we specify that to perform a move operation onIShape, we must call its virtual methodmove_to.
struct Circle : IShape {
float radius = 1;
Circle(float r) noexcept : radius(r) {}
auto get_area() const noexcept -> float override {
return PI * radius * radius;
}
void move_to(void* dst) && noexcept override {
new (dst) Circle(std::move(*this));
}
};
We define Circle an implementation of IShape whose area is defined by PI * radius².
struct Rectangle : IShape {
float width, height;
Rectangle(float w, float h) noexcept : width(w), height(h) {}
auto get_area() const noexcept -> float override { return width * height; }
void move_to(void* dst) && noexcept override {
new (dst) Rectangle(std::move(*this));
}
};
We define Rectangle an implementation of IShape whose are is defined by width * height.
struct RectangleEx : Rectangle {
RectangleEx(float w, float h) noexcept : Rectangle(w, h) {}
float angle = 0;
std::uint32_t color = 0xFFFFFFFF;
void move_to(void* dst) && noexcept override {
new (dst) RectangleEx(std::move(*this));
}
};
// RectangleEx is too large to be stored
static_assert(sizeof(RectangleEx) > ShapeMaxSize);
RectangleEx is an improvement of Rectangle.
But its added members make its size too big to fit in ShapeMaxSize bytes.
So it cannot be handled by Shape.
int main() {
Shape shape = Circle{1.414}; (1)
std::cout << shape->get_area() << " (expected: about PI*2 = 6.28)\n";
dynamic_cast<Circle&>(shape.get()).radius = 10; (2)
std::cout << shape->get_area() << " (expected: about PI*100 = 314)\n";
shape.emplace<Rectangle>(3, 4); (3)
std::cout << shape->get_area() << " (expected: 3*4 = 12)\n";
dynamic_cast<Rectangle&>(shape.get()).height *= 2; (4)
std::cout << shape->get_area() << " (expected: 3*8 = 24)\n";
std::cout << "Can handle RectangleEx ? " << std::boolalpha
<< Shape::can_handle_v<RectangleEx> << " (expected: false)\n";
return 0;
}
| 1 | Construct-moving a Circle of radius 1.414 (about sqrt(2)). |
| 2 | dynamic_cast does not throw: it is really a Circle that is stored. |
| 3 | Emplacing in place a Rectangle. This is allowed because Rectangle is nothrow-constructible. |
| 4 | dynamic_cast does not throw: it is really a Rectangle that is stored. |
Complete source
#include <iostream>
#include <jv/bounded-poly.hpp>
constexpr float PI = 3.14159265359f;
struct IShape {
virtual ~IShape() noexcept {}
virtual auto get_area() const noexcept -> float = 0;
virtual void move_to(void*) && noexcept = 0;
};
constexpr std::size_t ShapeMaxSize = 16;
using Shape = jv::BoundedPolyVM<std::aligned_storage_t<ShapeMaxSize>, IShape,
&IShape::move_to>;
// to specify the Shape type, we just needed to set a max size for storage, and
// to say which method must be called to move shapes.
struct Circle : IShape {
float radius = 1;
Circle(float r) noexcept : radius(r) {}
auto get_area() const noexcept -> float override {
return PI * radius * radius;
}
void move_to(void* dst) && noexcept override {
new (dst) Circle(std::move(*this));
}
};
struct Rectangle : IShape {
float width, height;
Rectangle(float w, float h) noexcept : width(w), height(h) {}
auto get_area() const noexcept -> float override { return width * height; }
void move_to(void* dst) && noexcept override {
new (dst) Rectangle(std::move(*this));
}
};
struct RectangleEx : Rectangle {
RectangleEx(float w, float h) noexcept : Rectangle(w, h) {}
float angle = 0;
std::uint32_t color = 0xFFFFFFFF;
void move_to(void* dst) && noexcept override {
new (dst) RectangleEx(std::move(*this));
}
};
// RectangleEx is too large to be stored
static_assert(sizeof(RectangleEx) > ShapeMaxSize);
int main() {
Shape shape = Circle{1.414}; (1)
std::cout << shape->get_area() << " (expected: about PI*2 = 6.28)\n";
dynamic_cast<Circle&>(shape.get()).radius = 10; (2)
std::cout << shape->get_area() << " (expected: about PI*100 = 314)\n";
shape.emplace<Rectangle>(3, 4); (3)
std::cout << shape->get_area() << " (expected: 3*4 = 12)\n";
dynamic_cast<Rectangle&>(shape.get()).height *= 2; (4)
std::cout << shape->get_area() << " (expected: 3*8 = 24)\n";
std::cout << "Can handle RectangleEx ? " << std::boolalpha
<< Shape::can_handle_v<RectangleEx> << " (expected: false)\n";
return 0;
}
Benchmark
The benchmark compares the performances of std::variant, std::unique_ptr and jv::utils::PolyStorage.
The program generates a sequence of operations (Addition, Substraction and ExclusiveXor) to be applied on an int.
The sequence is stored in a std::vector with space reserved outside of the time measurements.
The type stored in std::vector is either a std::variant, a std::unique_ptr or a jv::utils::PolyStorage.
The operation is applied using the visitor pattern for std::variant and polymorphic dispatch using unique_ptr and PolyStorage.
We measure 2 things: time of constructing the sequence of operations, and time of evaluating the sequence of operations.
These results are taken after compiling bounded-poly.cpp, variant.cpp and unique-ptr.cpp with g -std=c17 -O2.
Each result is taken as the minimum of three tries.
The sequence contains 100'000'000 operations.
| Which | Construction timing | Evaluation timing |
|---|---|---|
|
3.2718 seconds |
0.2755 seconds |
|
1.2204 seconds |
0.1900 seconds |
|
1.4158 seconds |
0.1846 seconds |
We see that unique_ptr is the worse alternative both for construction and evaluation.
This was expected, because at construction, it needs heap allocation, and at evaluation, it is not cache-friendly.
We see that variant and BoundedPoly have similar performances.
Comparaisons
-
I do prefer the calling syntax of polymorphism (as with
unique_ptrandBoundedPoly), instead ofstd::visitand lambdas (as withvariant). -
unique_ptrandvarianthave minimal bloat code in the class definition. ForBoundedPoly, we needed to define amove_tooperation for each derived class, which is indeed bloat code. It is not needed, by usingUniversalMoverinstead ofVirtualMover, but this adds space cost (one function pointer per BoundedPoly).
-
unique_ptris the most customizable, because it can stores any type derived fromBase. -
variantis the less customizable, because the types it accepts cannot be customized by a 3rd party. -
BoundedPolyis a good compromise, types being bounded only by their size and alignment.
-
unique_ptris heap allocated, so it is slow both to allocate and to access. -
variantandBoundedPolyare stack allocated, si it is cache-friendly and does not require system calls for construction.