Messing around in Visual Studio for a little while, I came up with this nonsense:
template<typename T>
class Matrix {
    std::vector<size_t> dimensions;
    std::unique_ptr<T[]> _data;
    template<typename ... Dimensions>
    size_t apply_dimensions(size_t dim, Dimensions&& ... dims) {
        dimensions.emplace_back(dim);
        return dim * apply_dimensions(std::forward<Dimensions>(dims)...);
    }
    size_t apply_dimensions(size_t dim) {
        dimensions.emplace_back(dim);
        return dim;
    }
public:
    Matrix(std::vector<size_t> dims) : dimensions(std::move(dims)) {
        size_t size = flat_size();
        _data = std::make_unique<T[]>(size);
    }
    template<typename ... Dimensions>
    Matrix(size_t dim, Dimensions&&... dims) {
        size_t size = apply_dimensions(dim, std::forward<Dimensions>(dims)...);
        _data = std::make_unique<T[]>(size);
    }
    T & operator()(std::vector<size_t> const& indexes) {
        if(indexes.size() != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        return _data[get_flat_index(indexes)];
    }
    T const& operator()(std::vector<size_t> const& indexes) const {
        if (indexes.size() != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        return _data[get_flat_index(indexes)];
    }
    template<typename ... Indexes>
    T & operator()(size_t idx, Indexes&& ... indexes) {
        if (sizeof...(indexes)+1 != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        size_t flat_index = get_flat_index(0, idx, std::forward<Indexes>(indexes)...);
        return at(flat_index);
    }
    template<typename ... Indexes>
    T const& operator()(size_t idx, Indexes&& ... indexes) const {
        if (sizeof...(indexes)+1 != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        size_t flat_index = get_flat_index(0, idx, std::forward<Indexes>(indexes)...);
        return at(flat_index);
    }
    T & at(size_t flat_index) {
        return _data[flat_index];
    }
    T const& at(size_t flat_index) const {
        return _data[flat_index];
    }
    size_t dimension_size(size_t dim) const {
        return dimensions[dim];
    }
    size_t num_of_dimensions() const {
        return dimensions.size();
    }
    size_t flat_size() const {
        size_t size = 1;
        for (size_t dim : dimensions)
            size *= dim;
        return size;
    }
private:
    size_t get_flat_index(std::vector<size_t> const& indexes) const {
        size_t dim = 0;
        size_t flat_index = 0;
        for (size_t index : indexes) {
            flat_index += get_offset(index, dim++);
        }
        return flat_index;
    }
    template<typename ... Indexes>
    size_t get_flat_index(size_t dim, size_t index, Indexes&& ... indexes) const {
        return get_offset(index, dim) + get_flat_index(dim + 1, std::forward<Indexes>(indexes)...);
    }
    size_t get_flat_index(size_t dim, size_t index) const {
        return get_offset(index, dim);
    }
    size_t get_offset(size_t index, size_t dim) const {
        if (index >= dimensions[dim])
            throw std::runtime_error("Index out of Bounds");
        for (size_t i = dim + 1; i < dimensions.size(); i++) {
            index *= dimensions[i];
        }
        return index;
    }
};
Let's talk about what this code accomplishes.
//private:
    template<typename ... Dimensions>
    size_t apply_dimensions(size_t dim, Dimensions&& ... dims) {
        dimensions.emplace_back(dim);
        return dim * apply_dimensions(std::forward<Dimensions>(dims)...);
    }
    size_t apply_dimensions(size_t dim) {
        dimensions.emplace_back(dim);
        return dim;
    }
public:
    Matrix(std::vector<size_t> dims) : dimensions(std::move(dims)) {
        size_t size = flat_size();
        _data = std::make_unique<T[]>(size);
    }
    template<typename ... Dimensions>
    Matrix(size_t dim, Dimensions&&... dims) {
        size_t size = apply_dimensions(dim, std::forward<Dimensions>(dims)...);
        _data = std::make_unique<T[]>(size);
    }
What this code enables us to do is write an initializer for this matrix that takes an arbitrary number of dimensions.
int main() {
    Matrix<int> mat{2, 2}; //Yields a 2x2 2D Rectangular Matrix
    mat = Matrix<int>{4, 6, 5};//mat is now a 4x6x5 3D Rectangular Matrix
    mat = Matrix<int>{9};//mat is now a 9-length 1D array.
    mat = Matrix<int>{2, 3, 4, 5, 6, 7, 8, 9};//Why would you do this? (yet it compiles...)
}
And if the number and sizes of the dimensions is only known at runtime, this code will work around that:
int main() {
    std::cout << "Input the sizes of each of the dimensions.\n";
    std::string line;
    std::getline(std::cin, line);
    std::stringstream ss(line);
    size_t dim;
    std::vector<size_t> dimensions;
    while(ss >> dim)
        dimensions.emplace_back(dim);
    Matrix<int> mat{dimensions};//Voila.
}
Then, we want to be able to access arbitrary indexes of this matrix. This code offers two ways to do so: either statically using templates, or variably at runtime.
//public:
    T & operator()(std::vector<size_t> const& indexes) {
        if(indexes.size() != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        return _data[get_flat_index(indexes)];
    }
    T const& operator()(std::vector<size_t> const& indexes) const {
        if (indexes.size() != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        return _data[get_flat_index(indexes)];
    }
    template<typename ... Indexes>
    T & operator()(size_t idx, Indexes&& ... indexes) {
        if (sizeof...(indexes)+1 != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        size_t flat_index = get_flat_index(0, idx, std::forward<Indexes>(indexes)...);
        return at(flat_index);
    }
    template<typename ... Indexes>
    T const& operator()(size_t idx, Indexes&& ... indexes) const {
        if (sizeof...(indexes)+1 != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        size_t flat_index = get_flat_index(0, idx, std::forward<Indexes>(indexes)...);
        return at(flat_index);
    }
And then, in practice:
Matrix<int> mat{6, 5};
mat(5, 2) = 17;
//mat(5, 1, 7) = 24; //throws exception at runtime because of wrong number of dimensions.
mat = Matrix<int>{9, 2, 8};
mat(5, 1, 7) = 24;
//mat(5, 2) = 17; //throws exception at runtime because of wrong number of dimensions.
And this works fine with runtime-dynamic indexing:
std::vector<size_t> indexes;
/*...*/
mat(indexes) = 54; //Will throw if index count is wrong, will succeed otherwise
There are a number of other functions that this kind of object might want, like a resize method, but choosing how to implement that is a high-level design decision. I've also left out tons of other potentially valuable implementation details (like an optimizing move-constructor, a comparison operator, a copy constructor) but this should give you a pretty good idea of how to start.
EDIT:
If you want to avoid use of templates entirely, you can cut like half of the code provided here, and just use the methods/constructor that uses std::vector<size_t> to provide dimensions/index data. If you don't need the ability to dynamically adapt at runtime to the number of dimensions, you can remove the std::vector<size_t> overloads, and possibly even make the number of dimensions a template argument for the class itself (which would enable you to use size_t[] or std::array[size_t, N] to store dimensional data).