6
\$\begingroup\$

This post has been answered and updated.

This was the first time I've ever written a template class. I've spent probably a bunch of times re-writing matrix code for different types, different functions. I'm looking to make a general library for future work of mine.

With that in mind, I'd like to have a nice working one to begin with that can be general. (i.e. handle different sizes appropriately).

-What is the best way to do that (I am using visual studio 2012)?

-Is there a way I can ensure the types being used are restricted to values safe for my implementation (currently, arithmetic heavy that would not be good for string data, for example).

-How should I properly handle errors? I string/silent failing is probably not appropriate.

-Any other general tips?

Thanks!

Matrix.h

#include <vector>
#include <iostream>

template <typename T> class Matrix;
template <typename T> std::ostream& operator<<(std::ostream& os, const Matrix<T>& rhs);

template<typename T>
class Matrix {
private: 
    std::vector<T> data;
    int rows;
    int cols;

public:
    Matrix();
    Matrix(std::vector<T>, int rows, int cols);
    void set(std::vector<T>, int rows, int cols);
    //Matrix(const Matrix<T>&);

    void print();
    Matrix<T> transpose();
    Matrix<T> dot(const Matrix<T> &);
    Matrix<T> add(const Matrix<T> &);
    Matrix<T> sub(const Matrix<T> &);
    Matrix<T> mult(const Matrix<T> &);
    Matrix<T> mult(const T &);
    bool isEqual(const Matrix<T> &);
    Matrix<T> concat(const Matrix<T> &);
    Matrix<T> stack(const Matrix<T> &);
    Matrix<T> kronecker(const Matrix<T> &);

    int getRows();
    int getCols();

    friend std::ostream& operator<< <>(std::ostream&, const Matrix<T> &);

    Matrix<T> operator+(const Matrix<T> &);
    Matrix<T> operator-(const Matrix<T> &);
    Matrix<T> operator*(const Matrix<T> &);
    Matrix<T> operator*(const T &);
    bool operator==(const Matrix<T> &);

};

/** Default Constructor

    creates an empty matrix

*/

template <typename T>
Matrix<T>::Matrix() {
    data.clear();
    rows = 0;
    cols = 0;
}

/** Constructor

    creates the matrix as the following:

    @params elements, - the elements of the matrix in Row-major form
            numRows, - the number of rows in the matrix
            numCols; - the number of coumns in the matrix

*/

template <typename T> 
Matrix<T>::Matrix(std::vector<T> elements, int numRows, int numCols) {
    rows = numRows;
    cols = numCols;

    data.clear();
    for(unsigned int i = 0; i < elements.size(); i++) {
        data.push_back(elements[i]);
    }
}

/** set

    resets the matrix to the input

    @params elements, - the elements of the matrix in Row-major form
            numRows, - the number of rows in the matrix
            numCols; - the number of coumns in the matrix
    @return void; nothing to return

*/

template <typename T> 
void Matrix<T>::set(std::vector<T> elements, int numRows, int numCols) {
    rows = numRows;
    cols = numCols;

    data.clear();
    for(unsigned int i = 0; i < elements.size(); i++) {
        data.push_back(elements[i]);
    }
}


/** operator+ (add)
    lhs + rhs;
    elementwise adition of rhs to lhs

    @params rhs; the matrix to add
    @return matrix; the sum

*/

template <typename T>
Matrix<T> Matrix<T>::operator+(const Matrix<T> & rhs) {
    return this->add(rhs);
}


/** operator- (subtract)
    lhs - rhs;
    elementwise subtraction of rhs from lhs

    @params rhs; the matrix to subtract
    @return matrix; the difference

*/

template <typename T>
Matrix<T> Matrix<T>::operator-(const Matrix<T> & rhs) {
    return this->sub(rhs);
}



/** operator(*) dot product 
    lhs * rhs;
    https://en.wikipedia.org/wiki/Matrix_multiplication
    calculate dot product of a matrix

    @params rhs; the second matrix
    @return matrix; the transformed product matrix

*/
template <typename T>
Matrix<T> Matrix<T>::operator*(const Matrix<T> & rhs) {
    return this->dot(rhs);
}

/** operator* (scalar multiplication)
    M<T> * T;
    calculate scalar product of a matrix

    @params rhs; the scalar;
    @return matrix; the transformed product matrix

*/
template <typename T>
Matrix<T> Matrix<T>::operator*(const T & T) {
    return this->mult(T);
}

/** operator ==

    elemetnwise comparison of two matrices of equal size 

    @params rhs; the second matrix
    @return bool; true if same size and elements all equal 

*/
template <typename T>
bool Matrix<T>::operator==(const Matrix<T> & rhs) {
    return this->isEqual(rhs);
}

/** ostream operator

    adds elements to output stream
    formatted 
     e11, e12
     e21, e22

     @params os, rhs; ostream refernece and matrix to output
     @return os, ostream reference

*/

template <typename T>
std::ostream& operator<<(std::ostream& os, const Matrix<T> & rhs) {
    for(unsigned int i = 0; i < rhs.data.size(); i++) {
        os << rhs.data[i] << "  ";
        if((i+1)%rhs.cols == 0) 
            os << std::endl;
    }
    return os;
}

/** isEqual

    elemetnwise comparison of two matrices of equal size 

    @params rhs; the second matrix
    @return bool; true if same size and elements all equal 

*/
template <typename T>
bool Matrix<T>::isEqual(const Matrix<T> & rhs) {


    if(rows != rhs.rows || cols != rhs.cols) {
        return false;
    }

    for(unsigned int i = 0; i < data.size(); i++) {
        if(data[i] != rhs.data[i]) 
            return false;
    }   

    return true;
}

/** add

    elementwise adition of rhs to lhs

    @params rhs; the matrix to add
    @return matrix; the sum

*/

template <typename T>
Matrix<T> Matrix<T>::add(const Matrix<T> & rhs) {


    if(rows != rhs.rows || cols != rhs.cols) {
        Matrix<T> matrix;
        return matrix;
    }

    std::vector<T> vec;
    for(unsigned int i = 0; i < data.size(); i++) {
        vec.push_back(data[i] + rhs.data[i]);
    }   

    return Matrix<T>(vec,rows,cols);
}

/** dot product 
    https://en.wikipedia.org/wiki/Matrix_multiplication
    calculate dot product of a matrix

    @params rhs; the second matrix
    @return matrix; the transformed product matrix

*/

template <typename T>
Matrix<T> Matrix<T>::dot(const Matrix<T> & rhs) {
    if(cols != rhs.rows) {
        std::cout << "Error! Can not resolve dot product on these matrices!" << std::endl;
        std::cout << "Requested: [" << rows << "x" << cols << "] <alt+7> [" << rhs.rows << "x" << rhs.cols << "]" << std::endl;
        Matrix<T> matrix;
        return matrix;
    }

    std::vector<T> vec;
    T sum = 0;
    for(int j = 0; j < rows; j++) {
        for(int k = 0; k < rhs.cols; k++) {
            for(int i = 0; i < cols; i++) {
                sum += data[i+j*cols] * rhs.data[k+i*rhs.cols];  
            }
            vec.push_back(sum);
            sum = 0;
        }
    }
    return Matrix(vec,rows,rhs.cols);
}

/** multiplication (Hardamard Product)
    https://en.wikipedia.org/wiki/Hadamard_product_(matrices)
    calculate elemetnwise product of a matrix

    @params rhs; the second matrix
    @return matrix; the transformed product matrix

*/

template <typename T>
Matrix<T> Matrix<T>::mult(const Matrix<T> & rhs) {
    if(rows != rhs.rows || cols != rhs.cols) {
        Matrix<T> matrix;
        return matrix;
    }

    std::vector<T> vec;
    for(unsigned int i = 0; i < data.size(); i++) {
        vec.push_back(data[i] * rhs.data[i]);
    }   

    return Matrix<T>(vec,rows,cols);
}

/** multiplication (scalar)

    calculate scalar product of a matrix

    @params rhs; the scalar;
    @return matrix; the transformed product matrix

*/

template <typename T>
Matrix<T> Matrix<T>::mult(const T & scalar) {

    std::vector<T> vec;
    for(unsigned int i = 0; i < data.size(); i++) {
        vec.push_back(data[i] * scalar);
    }   

    return Matrix<T>(vec,rows,cols);
}

/** subtract

    elementwise subtraction of rhs from lhs

    @params rhs; the matrix to subtract
    @return matrix; the difference

*/

template <typename T>
Matrix<T> Matrix<T>::sub(const Matrix<T> & rhs) {
    if(rows != rhs.rows || cols != rhs.cols) {
        Matrix<T> matrix;
        return matrix;
    }

    std::vector<T> vec;
    for(unsigned int i = 0; i < data.size(); i++) {
        vec.push_back(data[i] - rhs.data[i]);
    }   

    return Matrix<T>(vec,rows,cols);
}

template <typename T>
void Matrix<T>::print() {
    for(unsigned int i = 0; i < data.size(); i++) {
        std::cout << data[i] << ", ";
        if((i+1) % cols == 0)
            std::cout << std::endl;
    }
}

/** transpose

    Calculate transpose of matrix

    @return matrix; the transpose of this matrix

*/

template <typename T>
Matrix<T> Matrix<T>::transpose() {
    std::vector<T> vec;
    for(unsigned int i = 0; i < data.size(); i++) {
        vec.push_back(data[(cols*(i%rows)+i/rows)]);
    }
    return Matrix<T>(vec, cols, rows);
}

/** Concat

    append two matrices of equal row count

    @params rhs; the matrix to concatanate
    @return matrix; the contanated matrix

*/

template <typename T>
Matrix<T> Matrix<T>::concat(const Matrix<T> & rhs) {

    if(rows != rhs.rows) 
        return Matrix<T>(*this);

    std::vector<T> vec;
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            vec.push_back(data[i*cols + j]);
        }
        for(int j = 0; j < rhs.cols; j++) {
            vec.push_back(rhs.data[i*rhs.cols + j]);
        }
    }

    return Matrix<T>(vec,rows,cols+rhs.cols);
}

/** stack

    append two matrices of equal column count

    @params rhs; the matrix to stack below 
    @return matrix; the lhs stacked ontop of rhs matrix

*/

template <typename T>
Matrix<T> Matrix<T>::stack(const Matrix<T> & rhs) {

    if(cols != rhs.cols) 
        return Matrix<T>(*this);

    std::vector<T> vec;

    for(unsigned int i = 0; i < data.size(); i++) {
        vec.push_back(data[i]);
    }
    for(unsigned int i = 0; i < rhs.data.size(); i++) {
        vec.push_back(rhs.data[i]);
    }

    return Matrix<T>(vec,rows+rhs.rows,cols);
}

/** Kronecker
    https://en.wikipedia.org/wiki/Kronecker_product
    calculate kroncker product of two matrices

    @params rhs; the matrix operand
    @return matrix; the Kronecker product matrix

*/

template <typename T>
Matrix<T> Matrix<T>::kronecker(const Matrix<T> & rhs) {

    std::vector<T> vec;

    for(int i = 0; i < (rows*cols*rhs.rows*rhs.cols); i++) {
        int j = (i/rhs.cols)%cols + (i/(cols*rhs.rows*rhs.cols))*cols; //iterate lhs in proper order
        int k = (i%rhs.cols) + ((i / (cols * rhs.cols))%rhs.rows)*rhs.cols;  //iterate rhs in proper order
        //can use scalar multiplactions, matrix concat and stacking, but this is a single iteration through the vector.
        //Kronecker iterates both matrices in a pattern relative to the large product matrix.
        //std::cout << i << " : " << j << " : " << k << std::endl; 
        //std::cout << i << " : " << j << " : " << k << " : " << l << std::endl;
        vec.push_back(data[j]*rhs.data[k]);
    }

    return Matrix<T>(vec,rows*rhs.rows,cols*rhs.cols);
}

template <typename T>
int Matrix<T>::getRows() {
    return rows;
}

template <typename T>
int Matrix<T>::getCols() {
    return cols;
}

Source.cpp (Just testing implementation)

#include <iostream>
#include <vector>
#include "Matrix.h"
#include <string>
#include <fstream>
#include <sstream>



//void testMatrix(); //testing function.
//Matrix loadData(std::string); //Not implemented yet
//bool saveData(Matrix, std::string); //Not implemented yet


void testMatrixClass();

int main() {

    //testMatrix();
    testMatrixClass();

    return 0;
}


void testMatrixClass() {

    std::vector<Matrix<float>> testResults;
    std::vector<std::string> testInfo;
    std::vector<Matrix<int>> intTestResults;
    std::vector<std::string> intTestInfo;

    Matrix<float> temp;
    testResults.push_back(temp);
    testInfo.push_back("Default Constructor");

    std::vector<float> tempVec;
    for(int i = 0; i < 9; i++) {
        tempVec.push_back((float)(i%3));
    }

    Matrix<float> temp2(tempVec, 3, 3);
    testResults.push_back(temp2);
    testInfo.push_back("Vector constructor");

    testResults.push_back(temp2.transpose());
    testInfo.push_back("Vector transpose");

    tempVec.push_back(10.0);
    Matrix<float> temp3(tempVec, 5, 2);
    testResults.push_back(temp3);
    testInfo.push_back("Vector constructor");

    Matrix<float> temp4(temp3.transpose());

    testResults.push_back(temp4);
    testInfo.push_back("Vector transpose");

    testResults.push_back(temp2.dot(temp2));
    testInfo.push_back("Dot product");

    testResults.push_back(temp2.dot(temp3));
    testInfo.push_back("Error Dot Product");

    testResults.push_back(temp3.dot(temp4));
    testInfo.push_back("Dot product");

    testResults.push_back(temp2.add(temp2));
    testInfo.push_back("Add product");

    testResults.push_back(temp2.sub(temp2));
    testInfo.push_back("Sub product");

    testResults.push_back(temp2.mult(temp2));
    testInfo.push_back("hadamard product");

    testResults.push_back(temp2.mult(3));
    testInfo.push_back("scalar product");

    std::vector<int> tempInts;
    for(unsigned int i = 0; i < 9; i++) {
        tempInts.push_back((int)(i%3+i%4));
    }

    Matrix<int> intMatrix(tempInts,3,3);
    intTestResults.push_back(intMatrix);
    intTestInfo.push_back("Integer test");

    Matrix<int> intMatrix2 = intMatrix + intMatrix;
    intTestResults.push_back(intMatrix2);
    intTestInfo.push_back("Operator tests");

    Matrix<int> intMatrix3 = intMatrix * 2;
    intTestResults.push_back(intMatrix3);
    intTestInfo.push_back("Scalar Multiplacation");

    intTestResults.push_back(intMatrix2);
    if(intMatrix2 == intMatrix3) {
        intTestInfo.push_back("Boolean Comparison Successful");
    } else {
        intTestInfo.push_back("Boolean Comparison Failed");
    }

    intTestResults.push_back(intMatrix.concat(intMatrix2));
    intTestInfo.push_back("Concatanation Test");

    Matrix<int> intMatrix4 = intMatrix.stack(intMatrix2);
    intTestResults.push_back(intMatrix4);
    intTestInfo.push_back("Stack Test");

    intTestResults.push_back(intMatrix4.concat(intMatrix3));
    intTestInfo.push_back("Concat Error Check - Result should be [6x3] - (Prevents unequal row size)");

    intTestResults.push_back((intMatrix.concat(intMatrix2)).stack(intMatrix));
    intTestInfo.push_back("Stack Error Check - Result should be [3x6] - (Prevents unequal row size)");

    tempInts.clear();
    std::vector<int> tempInt2;
    for(unsigned int i = 0; i < 4; i++) {
        tempInts.push_back(i+1);

    }
    for(unsigned int i = 0; i < 6; i++) {
        tempInt2.push_back(i);
    }

    Matrix<int> intMatrix5(tempInts,2,2);

    tempInts.clear();
    tempInts.push_back(0);

    for(unsigned int i = 0; i < 3; i++) {
        tempInts.push_back(i+5);        
    }


    Matrix<int> intMatrix6(tempInts,2,2);
    intTestResults.push_back(intMatrix5);
    intTestInfo.push_back("Integer test");
    intTestResults.push_back(intMatrix6);
    intTestInfo.push_back("Integer test");
    intTestResults.push_back(intMatrix5.kronecker(intMatrix6));
    intTestInfo.push_back("Kroncker Test");

    Matrix<int> intMatrix7(tempInt2,2,3);
    intTestResults.push_back(intMatrix7);
    intTestInfo.push_back("Integer test");
    intTestResults.push_back(intMatrix5.kronecker(intMatrix7));
    intTestInfo.push_back("Kroncker Test");



    for(unsigned int i = 0; i < testResults.size(); i++) {
        std::cout << "Test: " << testInfo[i] << ": " << std::endl << testResults[i] << std::endl;
    }

    for(unsigned int i = 0; i < intTestResults.size(); i++) {
        std::cout << "Test: " << intTestInfo[i] << ": " << std::endl << intTestResults[i] << std::endl;
    }


}


//
//Matrix loadData(std::string) {
//  //TODO: Implement file loading and data parsing
//
//  Matrix matrix;
//  return matrix;
//}
//bool saveData(Matrix, std::string) {
//
//  return true;
//}
\$\endgroup\$
  • 2
    \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. I have rolled back your edits so that the accepted answer is not invalidated. \$\endgroup\$ – rolfl Nov 1 '18 at 11:07
6
\$\begingroup\$

Testing:

  • It's hard to notice problems in a long list of output like that.
  • It's hard to track the state of the tempVec and determine the correct output for each case.
  • Both these become harder if you have to come back to it later, making maintaining and changing the code changes difficult.

It's much neater to split the testing up, and calculate a simple boolean pass / fail result for each test. e.g.:

void test(bool condition, std::string const& testName)
{
    if (!condition)
        std::cout << "FAIL: " << testName << std::endl;
    else
        std::cout << "pass" << std::endl;
}

...

    {
        Matrix<float> m;
        test(m.getRows() == 0 && m.getCols() == 0, "Default constructed matrix is empty.");
    }

    {
        std::vector<float> args;
        for (auto i = 0; i != 9; ++i)
            args.push_back((float)(i % 3));

        Matrix<float> m(args, 3, 3);

        auto pass = true;
        for (auto c = 0; c != 3; ++c)
            for (auto r = 0; r != 3; ++r)
                pass = pass && (m.at(r, c) == c);

        test(pass, "Matrix from vector is correctly populated.");
    }

Even better, use a test framework like Google Test or Catch2.


Code:

  • Inside the class, the template argument is inferred by the compiler can be left off, e.g. Matrix<T> transpose(); means the same as Matrix transpose();.

  • The index type should be unsigned (a negative number of rows / columns doesn't make sense). std::size_t would be a good choice.

  • Does the size of the matrix actually need to vary at run-time? If not, many run-time errors can be ruled out by fixing the number of rows and columns at compile-time as template arguments, and using std::array instead of std::vector:

    template<typename T, std::size_t Rows, std::size_t Columns>
    class Matrix {
    private:
    
        std::array<T, Rows * Columns> data;
    
        ...
    };
    
  • Constructors should check that the number of values in the vector is appropriate for the number of rows / columns.

  • Constructors should use initializer lists to initialize class members in the constructor.

  • std::vector has its own copy constructor that copies the data, so there's no need to manually loop through (or clear the data member first). The constructor could look more like this:

    template <typename T>
    Matrix<T>::Matrix():
        data(), rows(0), cols(0) {
    }
    
    template <typename T>
    Matrix<T>::Matrix(std::vector<T> const& elements, int numRows, int numCols):
        data(elements), rows(numRows), cols(numCols) {
    
        if (!data.size() == rows * cols)
            throw std::invalid_argument("number of data elements must correspond to the number of rows and columns");
    }
    
  • Don't duplicate functionality. If you provide operator overloads for various operations there's no need to also provide named functions.

  • Nearly all the member functions can be declared const, ensuring they don't change member data. e.g. void print() const;, int getRows() const; etc.

  • Abstract the indexing of data elements into a separate function (e.g. int getIndex(int row, int col);). It's less error prone, and can easily be changed if necessary (i.e. row-major to column major storage).

  • The current interface is rather incomplete:

    • There is no way to access individual elements. (i.e. T& at(int row, int col); and T const& at(int row, int col); const).

    • Missing a != operator to go with the ==.

    • Several mathematical operations are missing. A more complete set of operators might look something like this:

      // member functions:
      
      Matrix& operator+=(T a);
      Matrix& operator-=(T a);
      Matrix& operator*=(T a);
      Matrix& operator/=(T a);
      
      Matrix& operator+=(Matrix const& a);
      Matrix& operator-=(Matrix const& a);
      
      Matrix operator-() const; // unary negation
      
      // free functions:
      
      Matrix<T> operator+(Matrix<T> const& a, T b);
      Matrix<T> operator-(Matrix<T> const& a, T b);
      Matrix<T> operator*(Matrix<T> const& a, T b);
      Matrix<T> operator/(Matrix<T> const& a, T b);
      
      Matrix<T> operator+(T a, Matrix<T> const& b);
      Matrix<T> operator-(T a, Matrix<T> const& b);
      Matrix<T> operator*(T a, Matrix<T> const& b);
      Matrix<T> operator/(T a, Matrix<T> const& b);
      
      Matrix<T> operator+(Matrix<T> const& a, Matrix<T> const& b);
      Matrix<T> operator-(Matrix<T> const& a, Matrix<T> const& b);
      
      Matrix<T> operator*(Matrix<T> const& a, Matrix<T> const& b);
      
      Vector<T> operator*(Matrix<T> const& a, Vector<T> const& b);
      Vector<T> operator*(Vector<T> const& a, Matrix<T> const& b);
      

    You might not want or need all of them, and it's generally best to follow the YAGNI principle (don't implement it if You Aren't Gonna Need It), but the operators should at least be symmetrical with things like matrix * scalar multiplication, and implement corresponding +=, -= operators for +, -, etc.


Questions:

  • I wouldn't worry about restricting types. The good thing about template code is that it will compile if a type supports the relevant interface, and not if it doesn't. However, if you really wanted to it could be done with std::enable_if and the various type traits, such as std::is_arithmetic.
  • For run-time errors, you can either #include <cassert> and use assert(condition);, or throw an exception. I would suggest using exceptions, since it's easier to test the code that throws them, and they are consistent between debug and release builds.
\$\endgroup\$
  • \$\begingroup\$ Thank you for the input. I'm not very familiar with std::array so I ended up just keeping the vector for now. Wasn't sure how to actually use the array in a class with unknown size, maybe a pointer to an array? Anyway, otherwise I've implemented most of the other ideas you included. I edited my post to include the updates, a different implementation driver that loads the testData file to read in the input data. Probably still not the most thorough.. I will probably look into catch2 or google test. Thanks again! \$\endgroup\$ – Chemistpp Nov 1 '18 at 1:11
4
\$\begingroup\$
Matrix(const std::vector<T> &, std::size_t rows, std::size_t cols);

The first argument would better be accepted by-value, i.e.

Matrix(std::vector<T> elements, std::size_t rows, std::size_t cols)
    : elements(std::move(elements)), rows(rows), cols(cols) {}

This way you cover construction both from lvalues and rvalues (the temporary elements is either move- or copy-constructed from the actual argument and then you only need to move from it.) Of course, you can have both:

Matrix(std::vector<T> const &elements, std::size_t rows, std::size_t cols)
    : elements(elements), rows(rows), cols(cols) {}
Matrix(std::vector<T> &&elements, std::size_t rows, std::size_t cols)
    : elements(std::move(elements)), rows(rows), cols(cols) {}

but in case of more than one potentially movable argument this gives you a combinatorial explosion of ctor overloads without any noticeable improvement.

Same goes for set().

And you shouldn't pass arguments of scalar types (std::size_t is one example) by reference, by-value would be cheaper:

T at(std::size_t, const std::size_t) const;
void &at(std::size_t, std::size_t, T value);

(I've changed value's type to the setter to T due to same considerations as above: now you can move from it.)

\$\endgroup\$
  • 2
    \$\begingroup\$ Unfortunately the OP edited their question significantly, but after the first answer was given, and before your reviewed the question. Protocol dictates that the edits be rolled back, which unfortunately significantly impacts your answer. Hopefully the OP will post a follow-up question with their revised code, and that may make it possible for your answer to be applied to that post. \$\endgroup\$ – rolfl Nov 1 '18 at 11:08
  • \$\begingroup\$ Well I have only commented on the question's current state. :) \$\endgroup\$ – bipll Nov 1 '18 at 11:51
  • \$\begingroup\$ I'll re-post updates tonight as new topic. \$\endgroup\$ – Chemistpp Nov 2 '18 at 15:13
  • \$\begingroup\$ @bipll codereview.stackexchange.com/questions/206852/… \$\endgroup\$ – Chemistpp Nov 3 '18 at 2:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.