Matrices show up quickly once code starts working with images, geometry, transforms, filters, simulation data, or small numerical tools. The math is usually not the hard part at first. The hard part is often representing the data in a way that does not turn every operation into pointer bookkeeping.
This article builds a small matrix type in C++11. The goal is not to compete with libraries such as Eigen or OpenCV. It is to show the engineering shape of a useful implementation: clear ownership, predictable indexing, dimension checks, and a design that can grow into a template without rewriting the whole idea.
The examples use C++11 and can be compiled with a command such as:
|
1 2 3 4 |
g++ -std=c++11 matrix_example.cpp -o matrix_example |
Page 1 covers the storage problem and a simple integer matrix. Page 2 finishes the generic template class and adds common operations.
A Matrix Is Simple, But Its Storage Choice Matters
Conceptually, a matrix is a rectangular table:
|
1 2 3 4 5 6 |
1 2 3 4 5 6 7 8 9 |
That simplicity can hide a design problem. A matrix needs two-dimensional indexing, but computer memory is still one-dimensional. The implementation must decide how to map (row, column) into memory.
A common beginner implementation allocates one dynamic array per row and stores those row pointers in another dynamic array. It works for a small demonstration, but it creates several ownership points and several places where cleanup can fail.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
int** make_matrix(std::size_t rows, std::size_t cols) { int** matrix = new int*[rows]; for (std::size_t row = 0; row < rows; ++row) { matrix[row] = new int[cols]; } return matrix; } void delete_matrix(int** matrix, std::size_t rows) { for (std::size_t row = 0; row < rows; ++row) { delete[] matrix[row]; } delete[] matrix; } |
This code is useful for understanding how dynamic allocation works, but it is not a good default design for C++ application code. If one row allocation throws, the already allocated rows need cleanup. If the caller forgets delete_matrix, memory leaks. If the caller passes the wrong row count, deletion becomes unsafe. The matrix data is also spread across separate allocations, which is often worse for cache behavior than a single contiguous block.
Modern C++11 gives us better tools without making the code complicated.
Store The Matrix In One Vector
A practical matrix class can store all elements in one std::vector. The vector owns the memory, releases it automatically, copies safely, and moves efficiently in C++11 implementations. The matrix class only has to track the shape and translate (row, column) into a vector index.
For row-major storage, the index formula is:
|
1 2 3 4 |
index = row * number_of_columns + column |
So the 3 by 3 matrix above is stored like this:
|
1 2 3 4 |
data = [1, 2, 3, 4, 5, 6, 7, 8, 9] |
That is a much better ownership model. There is one allocation managed by std::vector, and the indexing rule is easy to review.
A Small Integer Matrix Class
Before using templates, it is worth building the non-template version once. This keeps the storage idea visible and avoids mixing two lessons at the same time.
The class below stores integers, checks matrix dimensions, and exposes operator() for natural two-dimensional indexing. The bounds check is deliberate. In small educational code, a clear exception is more useful than silent memory corruption.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#include <cstddef> #include <stdexcept> #include <vector> class IntMatrix { public: IntMatrix(std::size_t rows, std::size_t cols, int initial_value = 0) : rows_(rows), cols_(cols), data_(rows * cols, initial_value) { if (rows == 0 || cols == 0) { throw std::invalid_argument("matrix dimensions must be non-zero"); } } std::size_t rows() const { return rows_; } std::size_t cols() const { return cols_; } int& operator()(std::size_t row, std::size_t col) { return data_.at(index(row, col)); } const int& operator()(std::size_t row, std::size_t col) const { return data_.at(index(row, col)); } private: std::size_t index(std::size_t row, std::size_t col) const { if (row >= rows_ || col >= cols_) { throw std::out_of_range("matrix index out of range"); } return row * cols_ + col; } std::size_t rows_; std::size_t cols_; /* Good: one vector owns the matrix storage. */ std::vector<int> data_; }; |
This class already avoids the most fragile part of the old pointer-based approach. There is no custom destructor, no manual copy constructor, and no cleanup function. That is the C++11 style worth aiming for: let a standard library type own the resource, then build the domain behavior around it.
Filling And Printing The Matrix
With operator(), client code can still look like ordinary matrix code. The object handles the memory layout internally.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> IntMatrix matrix(3, 3); int value = 1; for (std::size_t row = 0; row < matrix.rows(); ++row) { for (std::size_t col = 0; col < matrix.cols(); ++col) { matrix(row, col) = value++; } } for (std::size_t row = 0; row < matrix.rows(); ++row) { for (std::size_t col = 0; col < matrix.cols(); ++col) { std::cout << matrix(row, col) << ' '; } std::cout << '\n'; } |
The output is:
|
1 2 3 4 5 6 |
1 2 3 4 5 6 7 8 9 |
This is already a cleaner design than passing around three separate values such as int**, rows, and cols. The shape travels with the data, and invalid indexing fails at the place where the mistake happens.
Adding A Basic Operation
Matrix addition only makes sense when both matrices have the same shape. A class is a good place to enforce that rule because every caller should not have to remember the same check.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
IntMatrix add(const IntMatrix& left, const IntMatrix& right) { if (left.rows() != right.rows() || left.cols() != right.cols()) { throw std::invalid_argument("matrix sizes do not match"); } IntMatrix result(left.rows(), left.cols()); for (std::size_t row = 0; row < left.rows(); ++row) { for (std::size_t col = 0; col < left.cols(); ++col) { result(row, col) = left(row, col) + right(row, col); } } return result; } |
Returning result by value is fine here. C++11 compilers can move the vector storage when needed, and many cases are optimized further by return value optimization. Writing this as an output parameter just to avoid a return often makes the call site worse without improving real performance.
What Page 1 Fixed
The important change is not that the code became shorter. The important change is that ownership became obvious. The matrix owns a vector. The vector owns the memory. The indexing function owns the row-major mapping. Dimension checks sit next to the operation that requires them.
That structure is what lets us move to templates without duplicating the same code for float, double, or other arithmetic types.
