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_ptr
and 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_ptr
andBoundedPoly
), instead ofstd::visit
and lambdas (as withvariant
). -
unique_ptr
andvariant
have minimal bloat code in the class definition. ForBoundedPoly
, we needed to define amove_to
operation for each derived class, which is indeed bloat code. It is not needed, by usingUniversalMover
instead ofVirtualMover
, but this adds space cost (one function pointer per BoundedPoly).
-
unique_ptr
is the most customizable, because it can stores any type derived fromBase
. -
variant
is the less customizable, because the types it accepts cannot be customized by a 3rd party. -
BoundedPoly
is a good compromise, types being bounded only by their size and alignment.
-
unique_ptr
is heap allocated, so it is slow both to allocate and to access. -
variant
andBoundedPoly
are stack allocated, si it is cache-friendly and does not require system calls for construction.