HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

Storing collections of objects of any type

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
objectsanytypecollectionsstoring

Problem

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
#include
#include
#include

// TypeEraser.h

template
struct TypeEraser {
public:
template
TypeEraser(T *typeTag); // (1)

TypeEraser(TypeEraser &&other);

~TypeEraser();

TypeEraser(TypeEraser const &) = delete;
TypeEraser &operator = (TypeEraser const &) = delete;

template
T &get();

private:
void (_deleter)(char s); // (2)
void (*_mover)(TypeEraser &self, TypeEraser &&other); //
alignas(Talign) char _storage[Tsize]; //
};

template
using TypeEraserFor = TypeEraser;

template
template
TypeEraser::TypeEraser(T*)
: _deleter([](char *s) {
reinterpret_cast(s)->~T(); // (4)
})
, _mover([](TypeEraser &self, TypeEraser &&other) {
new (self._storage) T(std::move(other.get()));
}) {
static_assert(sizeof(T) == Tsize, "Wrong object size !"); // (3)
static_assert(alignof(T) == Talign, "Wrong object alignment !"); //
new (_storage) T;
}

template
template
T &TypeEraser::get() {
#pragma GCC diagnostic ignored "-Wstrict-aliasing"
return *reinterpret_cast(_storage);

Solution

This looks pretty solid. I have only some minor comments about your current implementation, and just a thought for an alternate one. First the thought.

Boost.Any

In case you don't want to reinvent the wheel, you can accomplish all of what you need with a vector. The main difference in implementation would be in your all(), which would look something like:

template 
std::vector &MultiVector::all() {
    std::size_t tId = typeId();
    assert(tId {});
    }

    return *unsafe_any_cast>(&subVectors[tId]);
}


We know what type it is, so we can use unsafe_any_cast - which just does a static_cast. The advantage here is that we're just using pre-existing code.

Construction

Right now, your TypeEraser takes a T* that is always a null pointer and then you always default construct a T in it. We can make this more functional. First, passing a pointer just as a tag is confusing. Let's create a tag type for this:

template  struct tag;

template 
struct TypeEraser {
    template 
    TypeEraser(tag );
};


The body is basically the same, we're just passing a type that is more clearly indicative of what we're actually doing.

Now, we can generalize this to add arbitrary args:

template 
TypeEraser(tag, Args&&... args ) {
    // ...
    new (_storage) T(std::forawrd(args)...);
}


And then add single-argument constructors to boot:

template 
TypeEraser(T&& arg)
: TypeEraser(tag>{}, std::forward(arg))
{ }


That will let us do either:

subVectors.emplace_back(std::vector{});
subVectors.emplace_back(tag>{});


Both of which I prefer to static_cast with nullptr.

Add

We have:

template 
T &add(T &&orig);


This has potential issues compared to emplace, where we have to specify the type T. Consider:

mv.add(4);
mv.add(5u);


That adds to two different type vectors internally. Is that intended by the user? Does it make sense? I don't know. Perhaps consider requiring the user to write:

mv.add(4);
mv.add(5u);


I'm not saying you should do this, but it's worth considering and explicitly ruling out.

Move Assignment

I would explicitly delete it:

TypeEraser& operator=(TypeEraser&& ) = delete;


Generalizing

So where do we go from here? We have aligned storage, and we have function pointers that are appropriately set for deleting and move-constructing. What if we want to support copying? Copy-assigning? Streaming? I'd suggest creating some policies:

template 
struct TypeEraser : Something;


where each Policy would define whatever valid operations via function pointers. We could have policies like copyable or movable or streamable. Fun project. This is sort of how Boost.TypeErasure does it.

Code Snippets

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(std::vector<T>{});
    }

    return *unsafe_any_cast<std::vector<T>>(&subVectors[tId]);
}
template <typename > struct tag;

template <std::size_t Tsize, std::size_t Talign>
struct TypeEraser {
    template <class T>
    TypeEraser(tag<T> );
};
template <class T, class... Args>
TypeEraser(tag<T>, Args&&... args ) {
    // ...
    new (_storage) T(std::forawrd<Args>(args)...);
}
template <typename T>
TypeEraser(T&& arg)
: TypeEraser(tag<std::remove_reference_t<T>>{}, std::forward<T>(arg))
{ }
subVectors.emplace_back(std::vector<T>{});
subVectors.emplace_back(tag<std::vector<T>>{});

Context

StackExchange Code Review Q#87729, answer score: 5

Revisions (0)

No revisions yet.