1. Homepage of Dr. Zoltán Porkoláb
    1. Home
    2. Archive
  2. Teaching
    1. Timetable
    2. Bolyai College
    3. C++ (for mathematicians)
    4. Imperative programming (BSc)
    5. Multiparadigm programming (MSc)
    6. Programming (MSc Aut. Sys.)
    7. Programming languages (PhD)
    8. Software technology lab
    9. Theses proposals (BSc and MSc)
  3. Research
    1. Sustrainability
    2. CodeChecker
    3. CodeCompass
    4. Templight
    5. Projects
    6. Conferences
    7. Publications
    8. PhD students
  4. Affiliations
    1. Dept. of Programming Languages and Compilers
    2. Ericsson Hungary Ltd

Student project in 2022

During the semester the students will (individually) work on a larger project. They will step by step create a Matrix class and its test environment. For every time you have to create a small additional task built on the previous results. We publish the solutions of every steps after some delay, so if you are disrupted for a while you still can catch up later.

Submit the solutions on Canvas as one single C++ source file containing the solution given in the task.

Task 1: Create multiplication functions for matrices

Write two functions, one for multiplication of a matrix by an integer, and another one for matrix by matrix multiplication. The matrix is represented as std::vector<std::vector<int>>. You have to create the two functions in a separate source file and you have to upload only this source into Canvas.

The following is a program to test your functions (don’t upload this).

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
#include <vector>

std::vector<std::vector<int>> // c*M
    mul(int c, std::vector<std::vector<int>> right);

std::vector<std::vector<int>> // M1*M2
    mul(std::vector<std::vector<int>> left,
        std::vector<std::vector<int>> right);

int main()
{
  const std::vector<std::vector<int>> m1 = {  // 2x5
    { 1, 2, 3, 4,  5 },
    { 6, 7, 8, 9, 10 }
  };  
  const std::vector<std::vector<int>> m2 = {  // 5x2
    { 1,  2 }, 
    { 3,  4 }, 
    { 5,  6 },
    { 7,  8 },
    { 9, 10 },
  };
  const std::vector<std::vector<int>> m3 = {  // 5x3
    {  1,  2,  3 }, 
    {  4,  5,  6 }, 
    {  7,  8,  9 },
    { 10, 11, 12 },
    { 13, 14, 15 },
  };

  std::vector<std::vector<int>> r1 = mul(  3, m1);  // 2x5
  std::vector<std::vector<int>> r2 = mul( m1, m2);  // 2x2
  std::vector<std::vector<int>> r3 = mul( m2, m1);  // 5x5
  std::vector<std::vector<int>> r4 = mul( m1, m3);  // 2x3

  const std::vector<std::vector<int>> res1 = {  // 2x5
    {  3,  6,  9, 12, 15 },
    { 18, 21, 24, 27, 30 }
  };  
  const std::vector<std::vector<int>> res2 = {  // 2x2
    {  95, 110 },
    { 220, 260 }
  };  
  const std::vector<std::vector<int>> res3 = {  // 5x5
    {  13,  16,  19,  22,  25 }, 
    {  27,  34,  41,  48,  55 },
    {  41,  52,  63,  74,  85 },
    {  55,  70,  85, 100, 115 },
    {  69,  88, 107, 126, 145 }
  };  
  const std::vector<std::vector<int>> res4 = {  // 2x3
    { 135, 150, 165 },
    { 310, 350, 390 }    
  };

  if (res1 == r1 && res2 == r2 && res3 == r3 && res4 == r4)
  {
    std::cerr << "Passed\n";
  }
  else  
  {
    std::cerr << "Failed\n";
  }
  return 0;	
}

At this point you can suppose that the matrix dimensions are correct, i.e. in matrix by matrix multiplication NxK and KxM (which results an NxM matrix).

Submit your solution to Canvas by 29. March, Tuesday, 23:59h.

Solution

This is one possible solution (not necessary the most optimized one). There might be many possible good solutions.

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
47
48
49
50
51
52
53
#include <iostream>
#include <vector>
#include <cassert>

void print(std::vector<std::vector<int>> vec)
{
  for ( int i = 0; i < vec.size(); ++i)
  {
    for ( int j = 0; j < vec[i].size(); ++j)
    {
      std::cout << vec[i][j] << " ";	    
    }	    
    std::cout << '\n';
  }
}

std::vector<std::vector<int>> mul(int c, 
                        std::vector<std::vector<int>> right)
{
  std::vector<std::vector<int>> result = right;
  for (int i = 0; i < right.size(); ++i) 
  {
    for ( int j = 0; j < right[i].size(); ++j)	  
    result[i][j] *= c;	  
  }
  return result;
}

std::vector<std::vector<int>> mul(
                        std::vector<std::vector<int>> left,
                        std::vector<std::vector<int>> right)
{
  assert(left[0].size() == right.size());

  std::vector<std::vector<int>> result;

  for ( int i = 0; i < left.size(); ++i)
  {
    std::vector<int> inner;
    for ( int j = 0; j < right[0].size(); ++j)
    {
      int sum = 0;
      for ( int k = 0; k < left[0].size(); ++k)
      {
        sum += left[i][k]*right[k][j];	      
      }      
      inner.push_back(sum);	    
    }
    result.push_back(inner);
  }
  // print(result);
  return result;	
}

Task 2: Create a class with operators

Create a Matrix class with the appropriate methods and operators. The user of the Matrix class looks like the following:

Remark: The comments in line 18 and 30 has been fixed, a new comment added to line 51.

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <vector>
#include "matrix.h"

int main()
{
  Matrix m1 = { { // 2x5
    { 1, 2, 3, 4,  5 },
    { 6, 7, 8, 9, 10 }
  } };  
  Matrix m2 = { {  // 5x2
    { 1,  2 }, 
    { 3,  4 }, 
    { 5,  6 },
    { 7,  8 },
    { 9, 10 },
  } };
  const Matrix m3 = { { // 5x3
    {  1,  2,  3 }, 
    {  4,  5,  6 }, 
    {  7,  8,  9 },
    { 10, 11, 12 },
    { 13, 14, 15 },
  } };

  Matrix r1 = m1.mul(3);   // 2x5
  Matrix r6 = -5 * m1;     // 2x5
  Matrix r2 = m1.mul(m2);  // 2x2
  Matrix r3 = m2 * m1;     // 5x5
  Matrix r4 = m1 * m3;     // 2x3

  const Matrix res1 = { {  // 2x5
    {  3,  6,  9, 12, 15 },
    { 18, 21, 24, 27, 30 }
  } };  
  const Matrix res6 = { {  // 2x5
    {  -5, -10, -15, -20, -25 },
    { -30, -35, -40, -45, -50 }
  } };  
  const Matrix res2 = { {  // 2x2
    {  95, 110 },
    { 220, 260 }
  } };  
  const Matrix res3 = { {  // 5x5
    {  13,  16,  19,  22,  25 }, 
    {  27,  34,  41,  48,  55 },
    {  41,  52,  63,  74,  85 },
    {  55,  70,  85, 100, 115 },
    {  69,  88, 107, 126, 145 }
  } };  
  const Matrix res4 = { { // 2x3
    { 135, 150, 165 },
    { 310, 350, 390 }    
  } };

  if ( res1 == r1 && res6 == r6 && 
       res2 == r2 && res3 == r3 && 
       res4 == r4)
  {
    std::cerr << "Passed\n";
  }
  else  
  {
    std::cerr << "Failed\n";
  }
  return 0;	
}

Define the class in matrix.h and implement the functions in matrix.cpp. The constructor should accept a const vector<vector>& parameter. Implement the mul(int) and mul(const Matrix&) memberfunctions with int and const Matrix& parameters. Implement the global +, - and * operators as well as the == and != operators.

Submit your solution as a single zip file which contains matrix.h and matrix.cpp to Canvas by 12 April, Tuesday 23:59.

Solution

We will create two files: martix.h for the class interface and matrix.cpp for the class implementation. Users of the Matrix class should include matrix.h and the whole project should compile and link matrix.cpp

Here is matrix.h:

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
#ifndef MATRIX_H   // usual include guards
#define MATRIX_H

#include <iostream>   // for print() and operator<<
#include <vector>     // for the data representation

class Matrix
{
public:	
  // constructor: initilaize data_ member with parameter vv
  Matrix(const std::vector<std::vector<int>>& vv) : data_(vv) { } 

  size_t rows() const { return data_.size(); }   // rows
  size_t cols() const {    // if we have no rows, let cols() == 0
       return data_.size() > 0 ? data_[0].size() : 0; }	
  void print(std::ostream&  os) const; // function to print to os

  Matrix mul(int c) const;           // multiplication by scalar
  Matrix mul(const Matrix& m) const; // multiplication by Matrix

  // operators m(i,j) to access [i][j] element
  int& operator()(int i, int j)       { return data_[i][j]; }
  int  operator()(int i, int j) const { return data_[i][j]; }
private:
  std::vector<std::vector<int>> data_;  // elements represented here
};
// global operators are also part of the interface
Matrix operator*( int, const Matrix& m);
Matrix operator+( const Matrix& m1, const Matrix& m2);
Matrix operator-( const Matrix& m1, const Matrix& m2);
Matrix operator*( const Matrix& m1, const Matrix& m2);

bool operator==( const Matrix& m1, const Matrix& m2);
bool operator!=( const Matrix& m1, const Matrix& m2);

std::ostream& operator<<( std::ostream& os, const Matrix& m);
#endif // MATRIX_H

Here is matrix.cpp, where we implement the member and global functions declared in matrix.h. Implementations are basically the same as for Task 1, but we pass parameters mostly by const reference const Matrix& to increase run-time efficiency.

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include <iostream>
#include <cassert>
#include "matrix.h"

void Matrix::print(std::ostream& os) const
{
  for ( size_t i = 0; i < rows(); ++i)
  {
    for ( size_t j = 0; j < cols(); ++j)
    {
      os << data_[i][j] << " ";	    
    }	    
    os << '\n';
  }
}

Matrix Matrix::mul(int c) const
{
  Matrix result = *this;
  for (size_t i = 0; i < result.rows(); ++i) 
  {
    for ( size_t j = 0; j < result.cols(); ++j)	  
    {
      result(i,j) *= c;	  
    }
  }
  return result;
}

Matrix Matrix::mul(const Matrix& right) const 
{
  assert(cols() == right.rows());

  std::vector<std::vector<int>> result; 
  for ( size_t i = 0; i < rows(); ++i)
  {
    std::vector<int> inner;
    for ( size_t j = 0; j < right.cols(); ++j)
    {
      size_t sum = 0;
      for ( size_t k = 0; k < cols(); ++k)
      {
        sum += data_[i][k]*right.data_[k][j];	      
      }      
      inner.push_back(sum);	    
    }
    result.push_back(inner);
  }
  return Matrix{result};	
}

Matrix operator*( int c, const Matrix& m)
{
  Matrix res{m};     // copy construct res
  return res.mul(c); // utilize member multiplication
}

Matrix operator+( const Matrix& m1, const Matrix& m2)
{
  assert( m1.cols() == 	m2.cols() && m1.rows() == m2.rows() );
  Matrix res{m1};
  for ( size_t i = 0; i < res.rows(); ++i)
  {
    for( size_t j = 0; j < res.cols(); ++j)
    {
      res(i,j) += m2(i,j); 	    
    }
  }
  return res;
}

Matrix operator-( const Matrix& m1, const Matrix& m2)
{
  assert( m1.cols() == 	m2.cols() && m1.rows() == m2.rows() );
  Matrix res{m1};
  for ( size_t i = 0; i < res.rows(); ++i)
  {
    for( size_t j = 0; j < res.cols(); ++j)
    {
      res(i,j) -= m2(i,j); 	    
    }
  }
  return res;
}

Matrix operator*( const Matrix& m1, const Matrix& m2)
{
  assert( m1.cols() == 	m2.rows() );
  Matrix res{m1};      // copy construct res
  return res.mul(m2);  // utilize member multiplication
}

bool operator==( const Matrix& m1, const Matrix& m2)
{
  if ( m1.rows() != m2.rows() || m1.cols() != m2.cols() )
    return false;   // not the same dimensions
  
  for ( size_t i = 0; i < m1.rows(); ++i)
  {
    for( size_t j = 0; j < m1.cols(); ++j)
    {
      if ( m1(i,j) != m2(i,j) )
	return false; 	  // at least one element is different
    }
  }
  return true;
}
bool operator!=( const Matrix& m1, const Matrix& m2)
{
  return ! (m1 == m2 );   // utilize operator==
}

std::ostream& operator<<( std::ostream& os, const Matrix& m)
{
  m.print(os);  // utilize member print
  return os;    // return left parameter, for printing in chain 
}

To use the matrix class one have to compile the matrix.cpp and link to the projects. Use the -c flag to compile only, and not to call the linker.

$ ls
main.cpp matrix.c matrix.h 
$ g++ -Wall -Wextra -c matrix.c
$ ls
main.cpp matrix.c matrix.h matrix.o
$ g++ -Wall -Wextra main.cpp matrix.o
$ a.out

When you compile the client code (which wants to use Matrix, e.g. main.cpp) add matrix.o to the compilation command, so the linker will bring the Matrix implementation to the executable.

Task 3: Create a class template from Matrix

Create a Matrix class template with the same methods and operators as for Task 2. The user of the Matrix looks like the following:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <iostream>
#include <vector>
#include "matrix.h"

int main()
{
  Matrix<int> m1 = { { // 2x5
    { 1, 2, 3, 4,  5 },
    { 6, 7, 8, 9, 10 }
  } };  
  Matrix<int> m2 = { {  // 5x2
    { 1,  2 }, 
    { 3,  4 }, 
    { 5,  6 },
    { 7,  8 },
    { 9, 10 },
  } };
  const Matrix<int> m3 = { { // 5x3
    {  1,  2,  3 }, 
    {  4,  5,  6 }, 
    {  7,  8,  9 },
    { 10, 11, 12 },
    { 13, 14, 15 },
  } };

  Matrix<int> r1 = m1.mul(3);   // 2x5
  Matrix<int> r6 = -5 * m1;     // 2x5
  Matrix<int> r2 = m1.mul(m2);  // 2x2
  Matrix<int> r3 = m2 * m1;     // 5x5
  Matrix<int> r4 = m1 * m3;     // 2x3

  const Matrix<int> res1 = { {  // 2x5
    {  3,  6,  9, 12, 15 },
    { 18, 21, 24, 27, 30 }
  } };  
  const Matrix<int> res6 = { {  // 2x5
    {  -5, -10, -15, -20, -25 },
    { -30, -35, -40, -45, -50 }
  } };  
  const Matrix<int> res2 = { {  // 2x2
    {  95, 110 },
    { 220, 260 }
  } };  
  const Matrix<int> res3 = { {  // 5x5
    {  13,  16,  19,  22,  25 }, 
    {  27,  34,  41,  48,  55 },
    {  41,  52,  63,  74,  85 },
    {  55,  70,  85, 100, 115 },
    {  69,  88, 107, 126, 145 }
  } };  
  const Matrix<int> res4 = { { // 2x3
    { 135, 150, 165 },
    { 310, 350, 390 }    
  } };

  // now test it for double parameters
  Matrix<double> md1 = { { // 2x5 
    { 1, 2, 3, 4,  5 },
    { 6, 7, 8, 9, 10 }
  } };  
  Matrix<double> rd1 = { { // 2x5 double
    { 0.5, 1.0, 1.5, 2.0, 2.5 },
    { 3.0, 3.5, 4.0, 4.5, 5.0 }
  } };  

  if ( res1 == r1 && res6 == r6 && 
       res2 == r2 && res3 == r3 && 
       res4 == r4 && 0.5*md1 == rd1
     )
  {
    std::cerr << "Passed\n";
  }
  else  
  {
    std::cerr << "Failed\n";
  }
  return 0;	
}

As now we define a class template, we create only the matrix.h header file, and define and implement the matrix class here. We do not create .cpp file for templates.

The constructor should accept a const vector<vector>& parameter. The Matrix data is now represented in a std::vector<std::vector>. The multiplication by scalar become multiplication by T, as mul(const T&).

DEADLINE EXTENDED! Submit your solution to Canvas copying the content of the only file: matrix.h by 10. May, Tuesday 23:59.


Financed from the financial support ELTE won from the Higher Education Restructuring Fund of the Hungarian Government.