C++ library for storing objects of different types contiguously. Header-only, zero dependencies, no exceptions, no rtti, C++17
Data locality can greatly improve the performance of an algorithm due to better CPU cache utilization. If the size of the data needed for one step of the algorithm is
known at compile time then data locality can easily be achieved by storing elements in a vector of structs. But if the size is known only at runtime then you might
find yourself writing rather hard to read and maintain memcpy
with offset code like this for storing data and reinterpret_cast'ing code for retrieving it. And what if you want to add new data in the middle,
increase the memory size or store non-trivial types? This library aims to provide a generalized abstraction for such tasks through a templated container type.
The library can be used as a single header file, through add_subdirectory
or through find_package
in a CMake project.
Simply copy the single header into your project and include it to use the library. You can also play with the library on compiler-explorer.
Clone the repository into a subdirectory of your CMake project. Then add it and link it to your target.
add_subdirectory(/path/to/repository/root)
target_link_libraries(your_app PUBLIC cntgs::cntgs)
Clone the repository and install the library
mkdir build
cd build
cmake -DCMAKE_INSTALL_PREFIX=/desired/installation/directory ..
cmake --build . --target install
Locate it and link it to your target.
# Make sure to set CMAKE_PREFIX_PATH to /desired/installation/directory
find_package(cntgs)
target_link_libraries(your_app PUBLIC cntgs::cntgs)
A full API reference documentation is still work-in-progress. Meanwhile, the following examples should help you get started.
The main workhorse of this library is the cntgs::ContiguousVector
, a container modelled after SequenceContainer. Start by defining a container capable of storing elements in your desired layout.
template <class... Parameter>
using ContiguousVector = cntgs::BasicContiguousVector<cntgs::Options<>, Parameter...>;
Each parameter must be a built-in or user-deinfed type, optionally wrapped into a parameter decorator. The vector stores objects of those types within one element in the order they are specified.
To store objects within one element of the cntgs::ContiguousVector
in the following layout, where f
denotes an object of type float
, u
denotes an object of type uint32_t
and |
the end of one element:
|fffuuuuuf|fffuuuuuf|fffuuuuuf|
using Vector = cntgs::ContiguousVector<cntgs::FixedSize<float>, //
cntgs::FixedSize<uint32_t>, //
float>;
The library employs special optimizations when all parameter are either plain types or cntgs::FixedSize
(optionally wrapped into cntgs::AlignAs
, see below).
To store objects within one element of the cntgs::ContiguousVector
in the following layout, where f
denotes an object of type float
, i
denotes an object of type int32_t
and |
the end of one element:
|iiiif|iif|iiif|iiiiiif|
Define the cntgs::ContiguousVector
as follows:
using Vector = cntgs::ContiguousVector<cntgs::VaryingSize<int32_t>, //
float>;
The exact signature of the constructor of a cntgs::ContiguousVector
depends on the provided template parameter. For a vector that in comprised of only plain types and cntgs::FixedSize
parameter the constructor takes the initial capacity and the number of objects per elements for each cntgs::FixedSize
parameter as arguments.
cntgs::ContiguousVector<cntgs::FixedSize<float>, //
cntgs::FixedSize<uint32_t>, //
float>
vector{initial_capacity, {first_object_count, second_object_count}};
For cntgs::ContiguousVector
with cntgs::VaryingSize
parameter the constructor takes the total number of bytes that all objects of cntgs::VaryingSize
parameter will need as an argument, in addition to the initial capacity.
cntgs::ContiguousVector<cntgs::VaryingSize<int32_t>, //
float> //
vector{initial_capacity, varying_object_count * sizeof(int32_t)};
cntgs::ContiguousVector
s that have both cntgs::FixedSize
and cntgs::VaryingSize
parameter, first take the total number of bytes of all objects of cntgs::VaryingSize
followed by the objects per elements for each cntgs::FixedSize
parameter.
cntgs::ContiguousVector<cntgs::FixedSize<std::unique_ptr<int32_t>>, //
cntgs::Varying<std::unique_ptr<uint32_t>>>
vector{initial_capacity, varying_object_count * sizeof(std::unique_ptr<uint32_t>), {fixed_object_count}};
Similar to the constructor, the exact signature of emplace_back
also depends on the parameter of the cntgs::ContiguousVector
. The objects of each parameter are constructed from one of the arguments provided to the function. For cntgs::FixedSize
parameter the argument can either be a range of an iterator. As an example for the vector defined above, assuming that the size of the first parameter is no less than three and the size of the second parameter no more than five.
std::vector first{1.f, 2.f, 3.f};
std::vector second{10u, 20u, 30u, 40u, 50u};
vector.emplace_back(first, second.begin(), 0.f);
For cntgs::VaryingSize
parameter the argument must be a range.
vector.emplace_back(std::vector{1, 2}, 10.f);
vector.emplace_back(std::vector{3, 4, 5}, 20.f);
To emplace move-only types either pass the range as an rvalue or wrap its iterator into a std::move_iterator.
std::vector first{std::make_unique<int32_t>(1u), std::make_unique<int32_t>(2u)};
std::vector second{std::make_unique<int32_t>(10), std::make_unique<int32_t>(20)};
vector.emplace_back(std::make_move_iterator(first.begin()), std::move(second));
Elements can be retrieved through the subscript operator, front()
or back()
of the cntgs::ContiguousVector
or by de-referencing its iterator. In either case a proxy reference is returned which is similar to a tuple of references.
auto&& [varying_int, the_float] = vector[0];
assert((std::is_same_v<cntgs::Span<int32_t>, decltype(varying_int)>));
assert((std::is_same_v<float&, decltype(the_float)>));
The objects of an individual parameter of one element can also be accessed using cntgs::get
.
auto&& objects_of_first_parameter = cntgs::get<0>(vector.front());
The cntgs::ContiguousVector
has additional member functions that behave very similar to their stl counterpart like pop_back
, reserve
, erase
, clear
, size
, capacity
, empty
, data
, get_allocator
, operator=
and operator<=>
. See the source file for more details.
The allocator used by the cntgs::ContiguousVector
can be changed to any allocator that fulfills the standard allocator requirements. The actual allocator used by the vector and returned by vector.get_allocator()
will be rebound to std::byte.
using Vector = cntgs::BasicContiguousVector< //
cntgs::Options<cntgs::Allocator<std::pmr::polymorphic_allocator<std::byte>>>, //
cntgs::FixedSize<uint32_t>, float>;
At construction pass the allocator as the last argument.
std::pmr::monotonic_buffer_resource resource;
Vector vector{initial_capacity, {fixed_object_count}, &resource};
The default alignment of objects stored in a cntgs::ContiguousVector
is one. Some types require an alignment larger than that. On most processors however, misaligned memory access works correctly and has basically no performance penalty. On some processors, like RISC, unaligned memory access is not supported at all.
If you need aligned objects because you want to load them into a SIMD register or because you have measured a performance penalty then specify the desired alignment with the cntgs::AlignAs
parameter decorator.
Each element in the following cntgs::ContiguousVector
is comprised of a fixed number of 32-byte aligned floats, followed by a variable number of 8-byte aligned integers, followed by one 8-byte aligned integer.
using Vector = cntgs::ContiguousVector< //
cntgs::FixedSize<cntgs::AlignAs<float, 32>>, //
cntgs::VaryingSize<cntgs::AlignAs<int32_t, 8>>, //
cntgs::AlignAs<int32_t, 8>>;