I have implemented a C++ Matrix class using std::vector and a number for rows/cols. The implementation works decently from the QA I've done. I have implemented a Vector class as a derived class but I'm unsure whether this implementation is optimal. Would it make more sense to make Vector the base class and Matrix the derived class with Vector being just m_data and Matrix adding m_nrow and m_ncol? Any general feedback more than welcome.
matrix.h:
#pragma once
#include <vector>
class Vector;
class Matrix
{
protected:
size_t m_nrow{};
size_t m_ncol{};
std::vector<double> m_data{};
public:
Matrix(const size_t nrow = 0, const size_t ncol = 0, const std::vector<double>& data = std::vector<double>{});
void clear();
void setElement(const double value, const size_t row_idx, const size_t col_idx);
void setElement(const double value, const size_t idx);
void removeRow(const size_t row_idx);
void removeCol(const size_t col_idx);
double& at(const size_t row_idx, const size_t col_idx);
double& at(const size_t idx);
double getElement(const size_t row_idx, const size_t col_idx) const;
double getElement(const size_t idx) const;
const std::vector<double>& getDataAsVector() const;
size_t nRow() const;
size_t nCol() const;
void print() const;
bool isSquare() const;
void operator+=(const Matrix& other_matrix);
void operator-=(const Matrix& other_matrix);
void operator*=(const double multiplier);
const Matrix operator+(const Matrix& other_matrix) const;
const Matrix operator-(const Matrix& other_matrix) const;
const Matrix operator*(const double multiplier) const;
const Matrix operator*(const Matrix& other_matrix) const;
const Vector operator*(const Vector& other_vector) const;
double operator[](const size_t idx) const;
};
class Vector : public Matrix
{
private:
using Matrix::isSquare; //hide this from the Vector class
public:
Vector(const std::vector<double>& data = std::vector<double>{});
double length() const;
double dotProduct(const Vector& other_vector) const;
double distanceTo(const Vector& other_vector) const;
const Vector operator+(const Vector& other_vector) const;
const Vector operator-(const Vector& other_vector) const;
const Vector operator*(const double multiplier) const;
};
matrix.cpp:
#include "matrix.h"
#include <iostream>
Matrix::Matrix(const size_t nrow, const size_t ncol, const std::vector<double>& data) //constructor, called when an object is created, don't include default vars here
: m_nrow{ nrow }, m_ncol{ ncol }, m_data{ data }
{
if ((nrow * ncol) != data.size())
throw std::invalid_argument("Data size does not match the dimensions!");
if ((nrow == 0 && ncol > 0) || (nrow > 0 && ncol == 0))
throw std::invalid_argument("One dimension is zero while the other one is not!");
}
void Matrix::clear()
{
m_nrow = 0;
m_ncol = 0;
m_data.clear();
}
void Matrix::setElement(const double value, const size_t row_idx, const size_t col_idx)
{
if (row_idx >= m_nrow || col_idx >= m_ncol)
throw std::invalid_argument("Index exceeds dimensions!");
m_data.at(row_idx * m_ncol + col_idx) = value;
}
void Matrix::setElement(const double value, const size_t idx)
{
if (idx >= m_nrow * m_ncol)
throw std::invalid_argument("Index exceeds dimensions!");
m_data.at(idx) = value;
}
void Matrix::removeRow(const size_t row_idx)
{
if (row_idx >= m_nrow)
throw std::invalid_argument("Index exceeds dimensions!");
else if (m_nrow == 1)
{
m_nrow = 0;
m_ncol = 0;
m_data.clear();
}
else
{
size_t idx_start{ row_idx * m_ncol };
m_data.erase(std::next(m_data.begin(), idx_start), std::next(m_data.begin(), idx_start + m_ncol));
m_nrow--;
if ((m_nrow * m_ncol) != m_data.size())
throw std::invalid_argument("Data size does not match the dimensions!");
}
}
void Matrix::removeCol(const size_t col_idx)
{
if (col_idx >= m_ncol)
throw std::invalid_argument("Index exceeds dimensions!");
else if (m_ncol == 1)
{
m_nrow = 0;
m_ncol = 0;
m_data.clear();
}
else
{
size_t idx_from_back{ (m_nrow - 1) * m_ncol + col_idx };
for (size_t i{ 0 }; i < m_nrow; i++)
{
m_data.erase(m_data.begin() + idx_from_back);
idx_from_back -= m_ncol;
}
m_ncol--;
if ((m_nrow * m_ncol) != m_data.size())
throw std::invalid_argument("Data size does not match the dimensions!");
}
}
double& Matrix::at(const size_t row_idx, const size_t col_idx)
{
if (row_idx >= m_nrow || col_idx >= m_ncol)
throw std::invalid_argument("Index exceeds dimensions!");
return m_data.at(row_idx * m_ncol + col_idx);
}
double& Matrix::at(const size_t idx)
{
if (idx >= m_nrow * m_ncol)
throw std::invalid_argument("Index exceeds dimensions!");
return m_data.at(idx);
}
double Matrix::getElement(const size_t row_idx, const size_t col_idx) const
{
if (row_idx >= m_nrow || col_idx >= m_ncol)
throw std::invalid_argument("Index exceeds dimensions!");
return m_data.at(row_idx * m_ncol + col_idx);
}
double Matrix::getElement(const size_t idx) const
{
if (idx >= m_nrow * m_ncol)
throw std::invalid_argument("Index exceeds dimensions!");
return m_data.at(idx);
}
const std::vector<double>& Matrix::getDataAsVector() const
{
return m_data;
}
size_t Matrix::nRow() const
{
return m_nrow;
}
size_t Matrix::nCol() const
{
return m_ncol;
}
void Matrix::print() const
{
std::cout << "Nrow: " << m_nrow << " Ncol: " << m_ncol << '\n';
std::cout << "Size: " << m_data.size() << " Capacity: " << m_data.capacity() << '\n';
size_t idx{ 0 };
for (size_t i{ 0 }; i < m_nrow; i++)
{
for (size_t j{ 0 }; j < m_ncol; j++)
{
std::cout << m_data.at(idx) << ' ';
idx++;
}
std::cout << '\n';
}
std::cout << '\n';
}
bool Matrix::isSquare() const
{
return (m_nrow == m_ncol);
}
void Matrix::operator+=(const Matrix& other_matrix)
{
if (m_ncol != other_matrix.m_ncol || m_nrow != other_matrix.m_nrow)
throw std::invalid_argument("Matrices have different dimensions!");
for (size_t i{ 0 }; i < m_data.size(); i++)
{
m_data.at(i) += other_matrix.m_data.at(i);
}
}
void Matrix::operator-=(const Matrix& other_matrix)
{
if (m_ncol != other_matrix.m_ncol || m_nrow != other_matrix.m_nrow)
throw std::invalid_argument("Matrices have different dimensions!");
for (size_t i{ 0 }; i < m_data.size(); i++)
{
m_data.at(i) -= other_matrix.m_data.at(i);
}
}
void Matrix::operator*=(const double multiplier)
{
for (size_t i{ 0 }; i < m_data.size(); i++)
{
m_data.at(i) *= multiplier;
}
}
const Matrix Matrix::operator+(const Matrix& other_matrix) const
{
if (m_ncol != other_matrix.m_ncol || m_nrow != other_matrix.m_nrow)
throw std::invalid_argument("Matrices have different dimensions!");
Matrix result{ *this };
result += other_matrix;
return result;
}
const Matrix Matrix::operator-(const Matrix& other_matrix) const
{
if (m_ncol != other_matrix.m_ncol || m_nrow != other_matrix.m_nrow)
throw std::invalid_argument("Matrices have different dimensions!");
Matrix result{ *this };
result -= other_matrix;
return result;
}
const Matrix Matrix::operator*(const double multiplier) const
{
Matrix result{ *this };
result *= multiplier;
return result;
}
const Matrix Matrix::operator*(const Matrix& other_matrix) const
{
if (m_ncol != other_matrix.m_nrow)
throw std::invalid_argument("Matrices are not compatible for multiplication!");
size_t sum_over{ m_ncol };
size_t nrow{ m_nrow };
size_t ncol{ other_matrix.m_ncol };
Matrix result{ nrow, ncol, std::vector<double>(nrow * ncol) };
for (size_t i{ 0 }; i < nrow; i++)
{
for (size_t j{ 0 }; j < ncol; j++)
{
double element_value{ 0.0 };
for (size_t k{ 0 }; k < sum_over; k++)
{
element_value += this->getElement(i, k) * other_matrix.getElement(k, j);
}
result.setElement(element_value, i, j);
}
}
return result;
}
const Vector Matrix::operator*(const Vector& other_vector) const
{
if (m_ncol != other_vector.m_nrow)
throw std::invalid_argument("Matrices are not compatible for multiplication!");
size_t sum_over{ m_ncol };
size_t nrow{ m_nrow };
std::vector<double> result(nrow);
for (size_t i{ 0 }; i < nrow; i++)
{
double element_value{ 0.0 };
for (size_t k{ 0 }; k < sum_over; k++)
{
element_value += this->getElement(i, k) * other_vector.getElement(k);
}
result.at(i) = element_value;
}
return Vector{ result };
}
double Matrix::operator[](const size_t idx) const
{
if (idx >= m_nrow * m_ncol)
throw std::invalid_argument("Index exceeds dimensions!");
return m_data.at(idx);
}
Vector::Vector(const std::vector<double>& data)
: Matrix{ data.size(), (data.size() == 0) ? 0 : 1, data }
{
}
double Vector::length() const
{
if (m_data.size() == 0)
throw std::invalid_argument("Vector is empty!");
double sum_of_squares{ 0.0 };
for (size_t i{ 0 }; i < m_data.size(); i++)
{
sum_of_squares += m_data.at(i) * m_data.at(i);
}
return std::sqrt(sum_of_squares);
}
double Vector::dotProduct(const Vector& other_vector) const
{
if (m_data.size() != other_vector.m_data.size())
throw std::invalid_argument("Vector don't have the same dimension!");
else if (m_data.size() == 0)
throw std::invalid_argument("Vectors are empty!");
double sum{ 0.0 };
for (size_t i{ 0 }; i < m_data.size(); i++)
{
sum += m_data.at(i) * other_vector.m_data.at(i);
}
return sum;
}
double Vector::distanceTo(const Vector& other_vector) const //const functions can be used by const class members
{
if (m_data.size() != other_vector.m_data.size())
throw std::invalid_argument("Vector don't have the same dimension!");
else if (m_data.size() == 0)
throw std::invalid_argument("Vectors are empty!");
double sum_of_squares{ 0.0 };
for (size_t i{ 0 }; i < m_data.size(); i++)
{
sum_of_squares += (m_data.at(i) - other_vector.m_data.at(i)) * (m_data.at(i) - other_vector.m_data.at(i));
}
return std::sqrt(sum_of_squares);
}
const Vector Vector::operator+(const Vector& other_vector) const
{
if (m_nrow != other_vector.m_nrow)
throw std::invalid_argument("Vectors have different dimensions!");
Vector result{ *this };
result += other_vector;
return result;
}
const Vector Vector::operator-(const Vector& other_vector) const
{
if (m_nrow != other_vector.m_nrow)
throw std::invalid_argument("Vectors have different dimensions!");
Vector result{ *this };
result -= other_vector;
return result;
}
const Vector Vector::operator*(const double multiplier) const
{
Vector result{ *this };
result *= multiplier;
return result;
}