Reputation: 576
I'm working on an entity-component system (ECS), inspired on Bitsquid's blog series. My ECS is composed of two main classes: System (responsible for creating/destroying entities) and Property (responsible for storing a std::vector of a component, for a given System).
When an Entity is created/destroyed, the System triggers some signals to keep the Property instances aware of the state-change on Systems, in order to keep the data contiguous and well-mapped.
Follow below a simple example of a system of cars, and each car has two properties: point2d position, std::string name.
System<Car> cars;
Property<Car, point2d> positions(cars);
Property<Car, std::string> names(cars);
auto car0 = cars.add();
auto car1 = cars.add();
positions[car0] = {0.0, 0.0};
names[car0] = "Car 0";
positions[car1] = {1.0, 2.0};
names[car1] = "Car 1";
This way I can keep the data in separate "arrays".
Let's suppose I want to add 1000 Entities. I could do it calling std::vector::reserve(1000)
on all of the properties, and then doing 1000 push_back
on each one of them.
I've identified a problem on such approach: I would need 1+N reserves
, if I have N properties. I was wondering if I could use a std::vector<tuple<point2d, std::string>>
for handling the memory allocations, but manage the data pretending (reinterpret cast?) I have the all the point2d
stored contiguously, followed by the strings.
This way I could take advantage of std::vector
api for saving notifying/reserve/resizing operations. Although I'd have to adapt the accessing methods (like vector::at, operator[], begin, end).
Do you have any ideas on how to achieve that? Or any alternate suggestions if you think this is not a good idea?
Upvotes: 0
Views: 758
Reputation: 576
I've (almost) finished my SoA implementation using std::vector
as underlying data structure.
Suppose we want to create a SoA for types <int, double, char>
. Instead of creating three vectors, one for each type, TupleVector<int, double, char>
class creates a single std::vector
. For data accessing/reserve/resize it plays with reinterpret cast, offset and tuple size calculations.
I just created a simple benchmark code that calls .resize(.size()+1)
10.000.000 times and my SoA implementation appears to be +-4x faster than using separate std::vectors
.
Here you can see the benchmark code: https://github.com/csguth/Entity/blob/development/src/Test/TupleVectorBenchmark.cpp
Although this implementation still needs some iterator capabilities (trivial) and more benchmarking.
Hope this can be useful to someone!!
Upvotes: 1
Reputation: 536
Seems that there is no simple way to make std::vector<T>
operate on some data other than it's own array of Ts. So, your options include searching for third party SoA (Stucture of Arrays) library, or making your own.
This might be a starting point for a SoA class, that mimics std::vector
's interface:
// This snippet uses C++14 features
#include <functional>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
template <typename F, typename... Ts, std::size_t... Is>
void tuple_for_each(std::tuple<Ts...>& tuple, F f, std::index_sequence<Is...>) {
using expander = int[];
(void)expander{0, ((void)f(std::get<Is>(tuple)), 0)...};
}
template <typename F, typename... Ts>
void tuple_for_each(std::tuple<Ts...>& tuple, F f) {
tuple_for_each(tuple, f, std::make_index_sequence<sizeof...(Ts)>());
}
// Missing in this example:
// - full support for std::vector's interface (iterators, exception safety guarantees, etc.);
// - access to individual homogeneous vectors;
// - lots of other things.
template <typename T, typename... Ts>
class soa_vector {
std::tuple<std::vector<T>, std::vector<Ts>...> data_;
template <std::size_t>
void push_back_impl() const {}
template <std::size_t position, typename Value, typename... Values>
void push_back_impl(Value&& value, Values&&... values) {
std::get<position>(data_).push_back(std::forward<Value>(value));
push_back_impl<position + 1, Values...>(std::forward<Values>(values)...);
}
template<std::size_t... Is>
std::tuple<std::add_lvalue_reference_t<T>, std::add_lvalue_reference_t<Ts>...>
tuple_at(std::size_t position, std::index_sequence<Is...>) {
return std::make_tuple(std::ref(std::get<Is>(data_)[position])...);
}
public:
template <typename... Values>
std::enable_if_t<sizeof...(Values) == sizeof...(Ts) + 1, void>
push_back(Values&&... values) {
push_back_impl<0, Values...>(std::forward<Values>(values)...);
}
void reserve(std::size_t new_capacity) {
tuple_for_each(data_, [new_capacity](auto& vec) { vec.reserve(new_capacity); });
}
std::size_t size() const { return std::get<0>(data_).size(); }
std::tuple<std::add_lvalue_reference_t<T>, std::add_lvalue_reference_t<Ts>...>
operator[](std::size_t position) {
return tuple_at(position, std::make_index_sequence<sizeof...(Ts) + 1>());
}
};
Upvotes: 1
Reputation: 16256
It's not possible, at least not with std::tuple
. You can consider tuple
as a simple struct
with members from tuple
s template args. This means they'll be aligned in memory each after another.
Instead all your managers can implement an interface that allows resizing (reserving) and you can register all your managers in er.. ManagerOfManagers
that will resize all of them in a loop
Upvotes: 2