简体   繁体   中英

Constructing array of C++ vectors of different types

Is there a nice way to construct an array of std::vectors of different types? Also Is there a good way of storing those vectors?

For example, let us have some structs Foo , Bar , and Baz . I want to make a container class Cont that hold some combination of Foo , Bar , Baz vectors. The following code will achieve this, but I have some problems with it.

#include <vector>

// arbitrary structs
struct Foo{ int var1; };
struct Bar{ double var1; double var2; };
struct Baz{ char var1; float var2; };

enum Mask{
    fooMask = (1<<0),
    barMask = (1<<1),
    bazMask = (1<<2)
};

class Cont{
    void** containers;

public:
    Cont(int mask){
        // count number of 1s in mask
        int count = 0;
        int countMask = mask;
        while(countMask){
            countMask &= countMask-1; // unset rightmost 1
            count++;
        }

        containers = new void*[count];

        int index = 0;
        if((mask & fooMask) == fooMask)
            containers[index++] = new std::vector<Foo>;
        if((mask & barMask) == barMask)
            containers[index++] = new std::vector<Bar>;
        if((mask & bazMask) == bazMask)
            containers[index++] = new std::vector<Baz>;
    }
};

int main(){
    // example construction
    Cont c1(fooMask);
    Cont c2(barMask|bazMask);

    return 0;
}

First, I don't like that I have to store the array of vectors in a void** but I could not figure out a better way.

Second, if I add a new struct called Qux , I would have to modify the Cont constructor. Preferably for maintainability, I would want to construct the array without having to hardcode the struct types into the Cont class.

I've tried to use templates to solve this problem, but couldn't find a solution I was happy with. I'm concerned about making Cont a template as I think it will lead to template bloat for each combination of structs. Also I will have multiple Cont objects but only one of each combination that I need.

You can use type erasure.

struct ContainerBase
{
  virtual ~ContainerBase() = 0;
  // This is where you can add an interface for common functionality.
  // Write the pure virtual functions here and implement/override them in ContainerTyped.
};

inline ContainerBase::~ContainerBase() = default;

template<class T>
struct ContainerTyped : public ContainerBase
{
  std::vector<T> values;
};

class Cont
{
  std::vector<std::unique_ptr<ContainerBase>> containers;

public:
  Cont(int mask) {
    // ...
    if ((mask & fooMask) > 0)
      containers.push_back(std::make_unique<ContainerTyped<Foo>>());
    if ((mask & barMask) > 0)
      containers.push_back(std::make_unique<ContainerTyped<Bar>>());
  }
};

Demo

This is probably more suitable than using eg std::any or other preexisting type erasure because 1) you specify that only specific things (your vector containers) can be stored, and 2) you can add a common interface as indicated and even specialize the interface functions in the differently ContainerTyped . But we would need to know more about your use case to detail this benefit.

The problem with void* is always that you need to somehow retain information about what you actually stored because you are circumventing the strong type system. In other words, how would you get the stored thing back into the strong type system? This is exactly the part where the above approach can shine because you could add a virtual print() = 0 in ContainerBase and then create specialized versions for each kind of struct, eg

template<>
void ContainerTyped<Foo>::print()
{
    for (Foo& foo : values) {
        // Print Foo objects as you wish!
    }
}

In terms of not having to touch the Cont constructor when adding a Qux struct, you obviously still need to encode the information of "which mask bit belongs to which struct" somehow, but you can extract it from the Cont constructor (and even hide it in a different translation unit):

// Put this implementation wherever, Cont only has to know the signature.
std::unique_ptr<ContainerBase> makeContainer(int mask, unsigned indexBit)
{
  if ((mask & fooMask) > 0)
    return std::make_unique<ContainerTyped<Foo>>();
  // etc.
  if ((mask & quxMask) > 0)
    return std::make_unique<ContainerTyped<Qux>>();
  return nullptr;
}

// ...

Cont::Cont(int mask)
{
  for (unsigned indexBit = 0; indexBit < 8; ++indexBit) {
    auto container = makeContainer(mask, indexBit);
    if (container)
      containers.emplace_back(std::move(container));
  }
}

You can go about other ways of encoding that enum -> type information, but that's beyond the scope of this question. The key is that you can hide your concrete type behind ContainerBase and use that everywhere you want to refer to "any one of those Containers".

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM