How can implement a C++ vector that points to other, multiply typed vectors?

290 views Asked by At

I want to store elements of multiple types in a single vector, while keeping elements of the same type contiguous. The types are derived from a base class and I expect different types to be implemented throughout the development cycle. For this reason it would help if the process of adding a new type to the list is very straightforward.

I can achieve this (to an extent) in the following manner:

//header
enum TypeID { TypeA_ID, TypeA_ID, TypeA_ID, TypeIDAmount };

vector<TypeA> vectorA;
vector<TypeB> vectorB;
vector<TypeC> vectorC;

//cpp
TypeBase* LookUp(TypeID type, int index)
{
    switch(type)
    {
    case TypeA_ID: return (TypeBase*) &vectorA[index];
    case TypeB_ID: return (TypeBase*) &vectorB[index];
    case TypeC_ID: return (TypeBase*) &vectorC[index];
    }
}

However this is not clean, easy to maintain, nor compile-friendly (the class holding the data is included in many places).

A more compile friendly (but uglier) option I thought was doing something like this

//header
void* vectorArray;

//cpp
void Initialize()
{
    vectorArray = new void*[TypeIDAmount];
    vectorArray[0] = new vector<TypeA>;
    vectorArray[1] = new vector<TypeB>;
    vectorArray[2] = new vector<TypeC>;
}

TypeBase* LookUp(TypeID type, int index)
{
    void* pTypedVector = &vectorArray[type];
    switch(type)
    {
    case TypeA_ID: return (TypeBase*) (*(vector<TypeA>*)pTypedVector)[index];
    case TypeB_ID: return (TypeBase*) (*(vector<TypeB>*)pTypedVector)[index];
    case TypeC_ID: return (TypeBase*) (*(vector<TypeC>*)pTypedVector)[index];
    }
}

(ew!)

Is there anything that would work generically like this?

vector< vector<?>* > vectorOfVariedVectors;

Edit:

The motivation for this structure is to store components in the Entity-Component design pattern.

The reason I want the Types (or rather, components) to be contiguous, is to be able to transverse them in a cache-friendly way. This means that I want the instances themselves to be contiguous. While using contiguous pointers would give me a behaviour similar to what I want, if they point to 'random' places in memory the cache misses will still happen when fetching their data.

Avoiding memory fragmentation is a nice additional benefit of this.

The main idea is to have a clean manager type class that holds and provides access to these elements. Having multiple member vectors that other developers would have to add to this class is undesirable as long as this requires users that create new classes to alter this manager class. Edits to this container class should be as simple as possible or hopefully, non-existent.

Solution Found

Thanks to Dmitry Ledentsov for pointing me to this article. This is pretty much what I was looking for.

1

There are 1 answers

3
5gon12eder On

As others have already pointed out in the comments, there is probably a better solution to your problem at a larger scale than the container you are looking for. Anyway, here is how you could do what you are asking for.

The basic idea is to store std::vectors of std::unique_ptrs of BaseTypes in a std::map with std::type_indexes as keys. The example uses C++11 features. Run-time error handling is omitted for brevity.

First, some headers:

#include <cstddef>      // std::size_t
#include <iostream>     // std::cout, std::endl
#include <map>          // std::map
#include <memory>       // std::unique_ptr
#include <sstream>      // std::ostringstream
#include <string>       // std::string
#include <type_traits>  // std::enable_if, std::is_base_of
#include <typeindex>    // std::type_index
#include <typeinfo>     // typid, std::type_info
#include <utility>      // std::move
#include <vector>       // std::vector

Next, let us define the class hierarchy. I will define an abstract base class and a template to make as many derived types as needed. It should be clear that the container works equally well for any other class hierarchy.

class BaseType
{

public:

  virtual ~BaseType() noexcept = default;

  virtual std::string
  name() const = 0;
};

template<char C>
class Type : public BaseType
{

private:

  const std::string name_;

public:

  Type(const std::string& name) : name_ {name}
  {
  }

  virtual std::string
  name() const final override
  {
    std::ostringstream oss {};
    oss << "Type" << C << "(" << this->name_ << ") @" << this;
    return oss.str();
  }
};

Now to the actual container.

class PolyContainer final
{

private:

  std::map<std::type_index, std::vector<std::unique_ptr<BaseType>>> items_ {};

public:

  void
  insert(std::unique_ptr<BaseType>&& item_uptr)
  {
    const std::type_index key {typeid(*item_uptr.get())};
    this->items_[key].push_back(std::move(item_uptr));
  }

  template<typename T,
           typename = typename std::enable_if<std::is_base_of<BaseType, T>::value>::type>
  BaseType&
  lookup(const std::size_t i)
  {
    const std::type_index key {typeid(T)};
    return *this->items_[key].at(i).get();
  }
};

Note that we have not even declared the possible sub-types of BaseType so far. That is, PolyContainer does not need to be altered in any way if a new sub-type is added.

I have made lookup a templated function because I find it much cleaner. If you don't want to do this, you can obviously add an additional std::type_info parameter and then use lookup(typid(SubType), 42) instead of lookup<SubType>(42).

Finally, let's use what we have.

using TypeA = Type<'A'>;
using TypeB = Type<'B'>;
using TypeC = Type<'C'>;
// As many more as you like...

int
main()
{
  PolyContainer pc {};
  pc.insert(std::unique_ptr<BaseType> {new TypeA {"first"}});
  pc.insert(std::unique_ptr<BaseType> {new TypeA {"second"}});
  pc.insert(std::unique_ptr<BaseType> {new TypeB {"third"}});
  pc.insert(std::unique_ptr<BaseType> {new TypeC {"fourth"}});
  pc.insert(std::unique_ptr<BaseType> {new TypeB {"fifth"}});
  std::cout << pc.lookup<TypeB>(0).name() << std::endl;
  std::cout << pc.lookup<TypeB>(1).name() << std::endl;
  return 0;
}