I have a class whose underlying data is a variant of std::vector
, std::unique_ptr
and std::deque
. This is shown in the code below.
template<class T>
class matrix2d
{
private:
typename std::variant<std::vector<T>,
std::unique_ptr<T[]>,
std::deque<T>> data;
public:
matrix2d<T>() = delete;
matrix2d<T>(size_t h, size_t w, int type) {
try {
switch (type) {
case 0:
data = std::vector<T>(h*w);
break;
case 1:
data = std::make_unique<T[]>(h*w);
break;
case 2:
data = std::deque<T>(h*w);
break;
default:
throw std::runtime_error("Unrecognized type of matrix2d class data");
}
}
catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
}
auto operator[](size_t i) {
return (std::begin(data) + i);
}
};
int main()
{
matrix2d<int> a2d(4,5,0);
for (size_t i{}; i<4; ++i) {
for (size_t j{}; j<5; ++j) {
a2d[i][j] = 5.0;
}
}
}
My questions are as follows:
Is it OK to create a union of a unique_ptr
and the other resizable containers?
Also, how can I overload the subscript operator [] to make the class function as a two-dimensional array?
The variant is valid.
The usage for accessing rows however requires different treatment for std::vector<T>
and std::unique_ptr<T[]>
on one hand and for std::deque<T>
on the other hand. As vector
and unique_ptr<T[]>
use contiguous memory, elements of a deque
are not required to be stored contiguously.
Let's assume we want first to implement operator[] only for vector and unique_ptr.
We need to add to the class a member that would hold the size of a row, this is required in order to calculate the position in the array of row number i.
So let's suppose you add a private member 'w':
size_t w;
and that you initialize it in the constructor:
matrix2d<T>(size_t h, size_t w, int type) : w(w) {
// ...
Now the operator[] that we are looking for can look like this:
auto operator[](size_t i) {
return std::visit([i, w = this->w](auto&& arg){return &arg[i*w];}, data);
}
The use of std::visit
is required even though we use the same operation here for any of the types managed by our std::variant
. However, std::visit
is also able to have different operation for each of the types stored, see std::visit on cppreference.
If we want to support deque
as well, this requires a different treatment.
Currently our operator[] returns T* and we want to keep it.
For deque, we cannot just take the address of the first element in a row and assume that all elements in the same row are adjacently stored right after, contiguously. So to allow the use of deque with the same approach we need at least the rows to be contiguous. We can achieve that with a deque of vectors of Ts. This may look like this in the variant
declaration in class:
std::variant< std::vector<T>,
std::unique_ptr<T[]>,
td::deque<std::vector<T>> > data;
And in the constructor, the initialization for the deque
would be:
case 2: { // TODO: use enum instead
auto d = std::deque<std::vector<T>>(h);
for(auto& item : d) {
item = std::vector<T>(w);
}
data = d;
}
break;
operator[] would be changed now to:
auto operator[](size_t i) {
return std::visit([i, w = this->w](auto&& arg){
using U = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<U, std::deque<std::vector<T>>>) {
return &arg[i][0];
}
else {
return &arg[i*w];
}
}, data);
}