1

Is there an idiomatic C++ type for a contiguous container that lets you specify both the value type (as std::vector and std::array do) and the index/size type?

I've writing some code that going to manipulate a lot of arrays of data, but the indexes of the various arrays may have different semantic meanings, and I want to use the type system to prevent me from accidentally using an index in the wrong context.

For example, if I'm managing a collection of n hats, and m cars, I can give each hat ids from 0 up to n-1 and each car ids from 0 up to m-1 and have various arrays of hat and car information, but I wouldn't want to accidentally use a car id to look up something in an array of hat information.

car_id model_t {0};
my::vector<hat_id, fabric> hat_materials { /*...*/ }
hat_materials[model_t]; // want this to be a compile error

Right now I'm on the fence between taking the constant-time hit and using std::unordered_map or spending some dev time wrapping std::vector in a custom container class that adds a second parameter.

gsamaras
  • 66,800
  • 33
  • 152
  • 256
rampion
  • 82,104
  • 41
  • 185
  • 301

2 Answers2

3

Is there an idiomatic C++ type for a contiguous container that lets you specify both the value type (as std::vector does) and the index/size type?

No.


What you face here is a typical decision we have to take, weighing in the trade-off between spending development time VS using what STL already provides us with.

In the first case you will have to spend some time, but hope to get better performance.

In the second case, your data structure is ready for use, but you may lose performance w.r.t. the data structure that you would have developed.

As you know, std::unordered_map, provides constant query time, thus I were you, I would go on with this data structure (personalizing it as much as I want, by providing custom entities (e.g. CoryKramer suggest hashing in the comments), and when the project completes, test the performance and seek the bottleneck of it. If it is caused by the unordered map, then - depending on the circumstances of your time, which you may not know exactly now - take action and develop a custom data structure that will do the trick, if needed.

gsamaras
  • 66,800
  • 33
  • 152
  • 256
0

You may create a class for that, something like:

template <typename IndexT, typename T>
class typed_vector
{
public:
    typed_vector() = default;

    typed_vector(const typed_vector&) = default;
    typed_vector(typed_vector&&) = default;

    typed_vector& operator=(const typed_vector&) = default;
    typed_vector& operator=(typed_vector&&) = default;

    template <typename ... U,
              typename ... Ts,
              std::enable_if_t<sizeof...(Ts) == 0
                               || !std::is_same<std::decay_t<U>>::value,
                                                typed_vector>* = nullptr>
    typed_vector(U&& u, Ts&&...args) : mData(std::forward<Ts>(args)...) {}

    typed_vector(std::initializer_list<T> ini) : mData(ini) {}


    // The safe array subscript
    const T& operator[](IndexT index) const {
        return mData[static_cast<int>(index)];
    }
    T& operator[](IndexT index) { return mData[static_cast<int>(index)]; }

    // ...
private:
    std::vector<T> mData;
};

So with

class hat_id
{
public:
    explicit hat_id(int id) : id(id) {}
    explicit operator int() const { return id; }
private:
    int id;
};

// Equivalent for car_id

You may have:

car_id model_t {0};
typed_vector<hat_id, fabric> hat_materials { /*...*/ }
hat_materials[model_t]; // Would be an error here.
Jarod42
  • 173,454
  • 13
  • 146
  • 250