Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cpp_demo #154

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
*.o
a.out
.DS_Store

mylist.c
*cpp_demo
!cpp_demo/
23 changes: 23 additions & 0 deletions M-cpp/cpp_demo/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
CC = g++
CXX = g++
CXXFLAGS = -std=c++11 -g -Wall $(INCLUDES)
LDFLAGS = -g

.PHONY: default

default: cpp_demo

cpp_demo: matrix.o

cpp_demo.o: matrix.hpp

matrix.o: matrix.hpp

.PHONY: clean

clean:
rm -f cpp_demo *.o

.PHONY: all

all: clean default
14 changes: 14 additions & 0 deletions M-cpp/cpp_demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Matrix Demo

This demo shows how a user can define a matrix class in C++.
Similar to Jae's `MyString`, `Matrix` class defined in `matrix.hpp` encapsulates all the memory management as seen in the constructor and destructor.
We added a few more functionalities such as overloading `operator+` to show how we can also implement addition of two matrices.
JamesYang007 marked this conversation as resolved.
Show resolved Hide resolved
Note that the user of this class `Matrix` does not need to know **how** memory management or addition are done,
but only that such functionalities exist.
This is one of the hallmarks of object-oriented programming (OOP).

### Matrix Addition
See the following [link](https://en.wikipedia.org/wiki/Matrix_(mathematics)#Addition,_scalar_multiplication,_and_transposition) for an explanation and an example of how to add two matrices.

## Run
Type `make` to build the executable and run `./cpp_demo`.
101 changes: 101 additions & 0 deletions M-cpp/cpp_demo/cpp_demo.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include <iostream>
#include "matrix.hpp"

static const std::string BARS = "===================";

// Prints testname formatted as such:
// ===================testname test===================
void print_test(const std::string& testname)
{
std::cout << BARS << testname << " test" << BARS << std::endl;
}

// Creates a matrix of size 3 x 2 and initializes entries.
// Returns the created matrix.
Matrix make_matrix()
{
Matrix M(3, 2);
M(0,0) = 1; M(0,1) = 3;
M(1,0) = 5; M(1,1) = -2;
M(2,0) = 9; M(2,1) = -5;
return M;
}

// Tests constructor.
// Prints the contents of a constructed and initialized matrix.
void test_ctor()
{
print_test("constructor");
Matrix M(1, 2);
M(0,0) = 6; M(0,1) = 9;
std::cout << "Printing M:" << std::endl;
M.print(); // prints the contents of M
}

// Tests copy constructor.
// Creates a copy constructed Matrix object from return value of make_matrix().
// Prints the contents of copy constructed object.
void test_cctor()
{
print_test("copy constructor");
Matrix M = make_matrix(); // copy constructed
std::cout << "Printing copy constructed matrix:" << std::endl;
M.print();
}

// Tests copy assignment.
// Creates Matrix M from return value of make_matrix(), Matrix A, and copy assigns M to A.
// Prints the contents of M and the contents of A before and after copy assignment.
void test_cass()
{
print_test("copy assignment");
Matrix M = make_matrix();
Matrix A(1, 2);
A(0,0) = 10; A(0,1) = -3;
std::cout << "Printing A:" << std::endl;
A.print();
std::cout << "Printing M:" << std::endl;
M.print();
std::cout << "Printing A after copy assigned from M:" << std::endl;
A = M; // copy assignment
A.print();
}

// Tests adding Matrix M with a nontrivial matrix.
// We chose the nontrivial matrix to be
// [
// 0, 1
// 1, 2
// 2, 3
// ]
void test_add()
{
print_test("add");
Matrix M = make_matrix();
Matrix N(3, 2);
N(0,0) = 0; N(0,1) = 1;
N(1,0) = 1, N(1,1) = 2;
N(2,0) = 2; N(2,1) = 3;

Matrix res = M + N;

std::cout << "Printing M:" << std::endl;
M.print();

std::cout << "Printing N:" << std::endl;
N.print();

std::cout << "Printing M + N:" << std::endl;
res.print();
}

int main()
{
// run tests
test_ctor();
test_cctor();
test_cass();
test_add();

return 0;
}
94 changes: 94 additions & 0 deletions M-cpp/cpp_demo/matrix.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#include <iostream>
#include <cassert>
#include "matrix.hpp"

Matrix::Matrix(int nrows, int ncols)
// good practice to initialize members regardless of constructor body
: nrows_(nrows), ncols_(ncols), data_(nullptr)
{
data_ = new int[nrows_ * ncols_];
}

Matrix::~Matrix()
{
delete[] data_;
}

Matrix::Matrix(const Matrix& m)
{
this->copy_from(m);
}

Matrix& Matrix::operator=(const Matrix& m)
{
// If m is the same object as current object,
// there is nothing to do but return reference to current object.
// If this check were not done and continued with code,
// we would free this->data_ and copy contents of m.data_,
// but m.data_ points to freed memory since m and *this are the same objects!
if (this == &m) {
JamesYang007 marked this conversation as resolved.
Show resolved Hide resolved
return *this;
}

// MUST free existing data before reassigning
// Otherwise, memory leak!
delete[] this->data_;
JamesYang007 marked this conversation as resolved.
Show resolved Hide resolved
this->copy_from(m);

return *this;
}

void Matrix::copy_from(const Matrix& m)
{
this->nrows_ = m.nrows_;
this->ncols_ = m.ncols_;
this->data_ = new int[this->nrows_ * this->ncols_];
std::copy(m.data_, m.data_ + m.nrows_ * m.ncols_, this->data_);
}

int& Matrix::operator()(int i, int j)
{
assert(i < nrows_ && j < ncols_);
return data_[i * ncols_ + j];
}

const int& Matrix::operator()(int i, int j) const
{
// Both idiomatic and safer to use const_cast to cast away constness
// than performing C-style casting like ((Matrix&) *this).
// Be very careful with const_cast; this is one of the few times that its usage is considered acceptable.
return const_cast<Matrix&>(*this)(i, j);
}

Matrix Matrix::operator+(const Matrix& m) const
{
// assert same sizes
assert(this->nrows_ == m.nrows_);
assert(this->ncols_ == m.ncols_);

Matrix res(this->nrows_, this->ncols_);

for (int i = 0; i < this->nrows_; ++i) {
for (int j = 0; j < this->ncols_; ++j) {
res(i,j) = (*this)(i,j) + m(i,j);
}
}

return res;
}

void Matrix::print() const
{
// Print dimensions of matrix
std::cout << nrows_ << " x " << ncols_ << std::endl;

// Print contents of matrix
std::cout << "[" << std::endl;
for (int i = 0; i < nrows_; ++i) {
for (int j = 0; j < ncols_; ++j) {
std::cout << (*this)(i,j) << ' ';
}
std::cout << std::endl;
}
std::cout << "]" << std::endl;
}
62 changes: 62 additions & 0 deletions M-cpp/cpp_demo/matrix.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// pragma once is the C++ way of include guards.
// This guarantees that header files will be included at most once.
#pragma once
JamesYang007 marked this conversation as resolved.
Show resolved Hide resolved

// Matrix is a class that represents a matrix of integers.
class Matrix
{
public:
Matrix() =default; // tells compiler to generate default constructor.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't you want to =delete this here? when would you want to declare Matrix M; on its own (and what dimensions would it have?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question, in practice having containers or other data structures using matrix comes up. If these data structures are default constructed, it will invoke default constructor on matrix. A lot of times, you can't initialize the matrices at the time of construction of the container. As an example, think about a graph implementation using adjacency matrix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are other ways of achieving the similar thing like putting default values for nrows = 0 and ncols = 0 instead, but I just chose this cuz it highlights usage of =default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo that's kinda beyond the scope of these notes – will defer to judgement of other reviewers. @lucieleblanc @themost1 @al3623 what are your thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this deeper reason for why I included it is way beyond the scope of class and actually totally subjective. For the students, I just want to say: I want a default constructor cuz I want it and here's a nice and simple way to let compiler generate it for you even if you defined your own constructor. The important thing is that you know how to do it.

Same thing with my decision to implement matrix with 1-dimensional array rather than like std::vector<std::vector>. I did that cuz it usually takes advantage of cache better, but all they need to know is I need something to hold the data and here's how you allocate/deallocate in ctor/dtor.

// Note that if user provides a constructor (as shown in the next line of code),
// compiler will not generate a default constructor!
// Any declarations such as:
// Matrix M;
// will raise a compiler error since there is no default constructor.
// Sometimes you may not want your Matrix to have this default constructor.
// In this case, you may omit this line, or
// as good practice, replace =default with =delete to be more explicit.
Matrix(int nrows, int ncols); // constructor
Matrix(const Matrix&); // copy constructor
~Matrix(); // destructor
Matrix& operator=(const Matrix&); // copy assignment operator

// operator() returns the row i, column j element of the matrix.
// For simplicity, it is defined such that if i >= number of rows or j >= number of columns,
// program crashes; in practice, it is best to raise an error.
// We return a reference to an internal data (int) so that we may write code as such:
//
// M(1, 2) = 69;
JamesYang007 marked this conversation as resolved.
Show resolved Hide resolved
//
// which assigns 69 to row 1, column 2 entry of matrix M.
// If operator() returns an int (no reference) the above code does not even compile.
int& operator()(int i, int j);
const int& operator()(int i, int j) const; // const version

// operator+ returns a new matrix that is the matrix addition of current matrix and matrix M.
// If the sizes do not match, program crashes; in practice, it is best to raise an error.
Matrix operator+(const Matrix& M) const;

// Prints contents of current matrix.
void print() const;

// Returns the number of rows.
int nrows() const
{
return nrows_;
}

// Returns the number of columns.
int ncols() const
{
return ncols_;
}

private:
// Helper function that deep copies another matrix into current matrix.
// This exact logic is used in copy constructor and copy assignment.
void copy_from(const Matrix&);

int nrows_; // number of rows
int ncols_; // number of columns
int* data_; // pointer to array containing data
};