I've put together a class whose goal is to store objects of arbitrary type, with each type having its own vector for contiguity. I'd like to gather advice on what I have so far before I carry on.
Let me show you the code, along with a main()
so you can compile and run it easily (you'll need C++14 support). Here it is on Coliru. I'll highlight some of the "interesting" points.
Note: this code has not been written is response to a real problem. It is an exploration of how much C++ allows us to streamline object management, and may produce an actual useful class. What I'm thinking about is a way of centralizing objects (say, game objects) library-side, and later be able to run code for some/all of them transparently.
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
// TypeEraser.h
template <std::size_t Tsize, std::size_t Talign>
struct TypeEraser {
public:
template <class T>
TypeEraser(T *typeTag); // (1)
TypeEraser(TypeEraser &&other);
~TypeEraser();
TypeEraser(TypeEraser const &) = delete;
TypeEraser &operator = (TypeEraser const &) = delete;
template <class T>
T &get();
private:
void (*_deleter)(char *s); // (2)
void (*_mover)(TypeEraser &self, TypeEraser &&other); //
alignas(Talign) char _storage[Tsize]; //
};
template <class Tarchetype>
using TypeEraserFor = TypeEraser< // (3)
sizeof(Tarchetype), //
alignof(Tarchetype) //
>;
template <std::size_t Tsize, std::size_t Talign>
template <class T>
TypeEraser<Tsize, Talign>::TypeEraser(T*)
: _deleter([](char *s) {
reinterpret_cast<T*>(s)->~T(); // (4)
})
, _mover([](TypeEraser &self, TypeEraser &&other) {
new (self._storage) T(std::move(other.get<T>()));
}) {
static_assert(sizeof(T) == Tsize, "Wrong object size !"); // (3)
static_assert(alignof(T) == Talign, "Wrong object alignment !"); //
new (_storage) T;
}
template <std::size_t Tsize, std::size_t Talign>
template <class T>
T &TypeEraser<Tsize, Talign>::get() {
#pragma GCC diagnostic ignored "-Wstrict-aliasing"
return *reinterpret_cast<T*>(_storage); // (4)
#pragma GCC diagnostic pop
}
template <std::size_t Tsize, std::size_t Talign>
TypeEraser<Tsize, Talign>::TypeEraser(TypeEraser &&other)
: _deleter(other._deleter)
, _mover(other._mover) {
_mover(*this, std::move(other));
}
template <std::size_t Tsize, std::size_t Talign>
TypeEraser<Tsize, Talign>::~TypeEraser() {
_deleter(_storage);
}
// MultiVector.h
struct MultiVector {
template <class T>
std::vector<T> &all();
template <class T, class... Args>
T &emplace(Args &&... args);
template <class T>
T &add(T &&orig);
private:
static std::size_t _tIdCounter;
template <class T>
static std::size_t typeId();
std::vector<TypeEraserFor<std::vector<char>>> subVectors;
};
template <class T>
std::size_t MultiVector::typeId() { // (5)
static std::size_t id = _tIdCounter++;
return id;
}
template <class T>
std::vector<T> &MultiVector::all() {
std::size_t tId = typeId<T>();
assert(tId <= subVectors.size());
if(tId == subVectors.size()) {
subVectors.emplace_back(static_cast<std::vector<T>*>(nullptr)); // (1)
}
return subVectors[tId].get<std::vector<T>>();
}
template <class T, class... Args>
T &MultiVector::emplace(Args &&... args) {
auto &allT = all<T>();
allT.emplace_back(std::forward<Args>(args)...);
return allT.back();
}
template <class T>
T &MultiVector::add(T &&orig) {
auto &allT = all<std::remove_reference_t<T>>();
allT.push_back(std::forward<T>(orig));
return allT.back();
}
// MultiVector.cpp
std::size_t MultiVector::_tIdCounter = 0u;
int main()
{
MultiVector mv;
for(int i = 0; i < 10; ++i)
mv.add(i);
mv.emplace<std::string>("Hello");
mv.emplace<std::string>("World");
mv.emplace<std::string>("!");
for(auto &i : mv.all<int>())
std::cout << i << ' ';
std::cout << '\n';
for(auto &s : mv.all<std::string>())
std::cout << s << ' ';
std::cout << '\n';
assert(mv.all<float>().empty());
return 0;
}
Highlights
- Tag dispatching: to construct a
TypeEraser
, I'm passing an unused pointer to its constructor so it can deduce its type. - Type erasure:
TypeEraser
stores the object in a properly (see 3.) sized and aligned memory segment. It also stores pointers to functions needed to delete the contained object , or constructing it by moving an existing one. - Assumptions about the contained object:
TypeEraser
is parameterized with the size and alignment of its storage, and checks that they match with the object you try to put in. The helper typeTypeEraserFor
takes in an "archetype", a type that you expect to share size and alignment with every other type you'll store in thisTypeEraser
. In this example, I assume that every specialization ofstd::vector
(that I'd want to use) shares the same size and alignment asstd::vector<char>
. - Placement
new
: according to the documentation foroperator new
on cppreference (5th overload), placementnew
returns its pointer argument unchanged. Thus, I'm assuming I can just retrieve said pointer "on the fly" by taking the address ofstorage
again. Am I right, and is GCC's type-punning warning a false positive indeed? I've also read aboutreinterpret_cast<T*>()
being an ill choice because it is implementation-defined. How aboutstatic_cast<T*>(static_cast<void*>())
? - Note that
TypeEraser
doesn't actually retain any more information on its content than how to move it and how to delete it. The user of the class is supposed to feed it back the type when she wants to retrieve its content. This is done byMultiVector
itself, using a handy trick about static variables inside templates: every time you instanciatetypeId()
(arguably a bad name...) with a new type, a new static variable gets initialized, increments the global counter, and is returned. Subsequent calls with the same type will retrieve the same static variable, and return the same ID.
Future expansion
The typeId()
trick is a neat one, but it works only in one direction (mainly because identifiers are generated at runtime in indeterminate order). Thus I'm afraid of throwing out too much type information. For example, a neat feature would be to iterate over every subclass-type vector given a superclass, which is not possible as-is. The only solution I'm seeing is declaring and keeping the full typelist of what can be stored in the MultiVector
. This implies a full enumeration of storable types, which would make it more rigid. I guess you can't have your cake and eat it, but I'd like to know if there are some possibilities between these extrema.
std::vector<boost::any>
doesn't? \$\endgroup\$