First, and very important, you know already about the std::vector
and even about 2d vectors, like std::vector<std::vector<int>>
. That is very good and makes life simple.
You know also about C++ extraction operator >>
and the inserter operator <<
. That is especially important for the proposed solution. You also know that these operators can be chained, because they always return a reference to the stream for which they are called. With that you can write things like
std::cout << numberOfRows << '\n' << numberOfColumns << '\n';
What will hapen here is:
First, std::cout << numberOfRows
will be executed and return std::cout
. The resulting statement will be
std::cout << '\n' << numberOfColumns << '\n';
Next, std::cout << '\n'
will be executed and return std::cout
. The resulting statement will be
std::cout << numberOfColumns << '\n';
And so on and so on. So, you can see that we can chain. Good.
For input using the extraction operator >>
we know that it will, per default settings, skip all white spaces. So, to read the number of rows and number of columns from your source input, you may simply write:
is >> numberOfRows >> numberOfColumns;
and we know the size of the matrix.
I will later explain that we do not even need this, because we "see", looking at the source data, that we have 3 rows and 4 columns.
Anyway, now we have the matrix size and we can use it. With the initial defined empty 2d-vector, we cannot do that much, so let's resize it to the given values. This will be done with the std::vector
s resize
command. Like this:
data.resize(numberOfRows, std::vector<int>(numberOfColumns));
The interesting point is that the 2d-vector internally knows the number of rows and columns. You can get this with the size()
function.
So, we could now read all data with a nested for loop:
for (unsigned int row = 0; row < m.numberOfRows; ++row)
for (unsigned int col = 0; col < m.numberOfColumns; ++col)
is >> m.data[row][col];
That is very simple and intuitive.
We can also use more modern range based for loops. Our vector knows its sizes internally and and simply iterate over all its data.
We just need to use references to be able to modify the data. So, we can also write:
// The vector will now know its size. So, we can use range based for loops to fill it
for (std::vector<int>& row : data) // Go over all rows
for (int& col : row) // For each column in a row
is >> col; // Read the value and put in matrix
This looks even simpler, and it works, because we use references.
Then, how to combine all this know how in a function. Luckily, C++ allows to overwrite the IO operators, e.g. the inserter operator >>
for our custom data type, our class. We just need to add:
friend std::istream& operator >> (std::istream& is, Matrix& m) {
to our class and implement the functionality described above.
The whole program example could then look like:
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
struct Matrix {
unsigned int numberOfRows{};
unsigned int numberOfColumns{};
std::vector<std::vector<int>> data{};
// Read matrix from any stream
friend std::istream& operator >> (std::istream& is, Matrix& m) {
// First read the number of rows and columns
is >> m.numberOfRows >> m.numberOfColumns;
// Now resize the vector to have the given size
m.data.clear();
m.data.resize(m.numberOfRows, std::vector<int>(m.numberOfColumns));
// The vector will now know its size. So, we can use range based for loops to fill it
for (std::vector<int>& row : m.data) // Go over all rows
for (int& col : row) // For each column in a row
is >> col; // Read the value and put in matrix
return is;
}
// Write matrix to any stream
friend std::ostream& operator << (std::ostream& os, const Matrix& m) {
os << m.numberOfRows << '\n' << m.numberOfColumns << '\n';
// Now output the matrix itself
for (const std::vector<int>& row : m.data) { // Go over all rows
for (const int& col : row) // For each column in a row
os << col << ' ';
os << '\n';
}
return os;
}
void readFromFile(const std::string& filename) {
// Open the file and check, if could be opened
std::ifstream ifs{ filename };
if (ifs) {
// Read complete matrix with above extractor operator
ifs >> *this;
}
else std::cerr << "\n*** Error: Could not open source file '" << filename << "' for reading\n";
}
void writeToFile(const std::string& filename) {
// Open the file and check, if could be opened
std::ofstream ofs{ filename };
if (ofs) {
// Read complete matrix with above extractor operator
ofs << *this;
}
else std::cerr << "\n*** Error: Could not open source file '" << filename << "' for writing\n";
}
};
// In this example, I will read from a stringstream, but reading from a file is the same
std::istringstream iss{ R"(
3
4
1 2 3 4
5 6 7 8
9 0 1 2
)" };
int main() {
Matrix matrix{};
// Read and parse the complete matrix
iss >> matrix;
// Debug output. Show matrix
std::cout << matrix;
}
This looks already good and can be extended as needed.
You remember that I said that there is no need to specify the numbers of rows and columns, because we "see" it. But how can our program see that?
Simple.
We first read a complete line, with for example "1 2 3 4" in it, this is a row. Then we put this string into a std::istringstream
and extract all values that we can get from their. This, we will do until end of file.
Very simple. Let me show you the modified example:
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
struct Matrix {
std::vector<std::vector<int>> data{};
// Read matrix from any stream
friend std::istream& operator >> (std::istream& is, Matrix& m) {
m.data.clear();
std::string line{};
// Read all lines, all rows of the matrix
while (std::getline(is,line) and not line.empty()) {
// Put this into a stringstream for further extraction
std::istringstream iss{ line };
// Add a new row to our 2d vector
m.data.push_back({});
// Now extract all values from the line and add it to the just created row
int value{};
while (iss >> value) {
m.data.back().push_back(value);
}
}
return is;
}
// Write matrix to any stream
friend std::ostream& operator << (std::ostream& os, const Matrix& m) {
// Now output the matrix itself
for (const std::vector<int>& row : m.data) { // Go over all rows
for (const int& col : row) // For each column in a row
os << col << ' ';
os << '\n';
}
return os;
}
void readFromFile(const std::string& filename) {
// Open the file and check, if could be opened
std::ifstream ifs{ filename };
if (ifs) {
// Read complete matrix with above extractor operator
ifs >> *this;
}
else std::cerr << "\n*** Error: Could not open source file '" << filename << "' for reading\n";
}
void writeToFile(const std::string& filename) {
// Open the file and check, if could be opened
std::ofstream ofs{ filename };
if (ofs) {
// Read complete matrix with above extractor operator
ofs << *this;
}
else std::cerr << "\n*** Error: Could not open source file '" << filename << "' for writing\n";
}
};
// In this example, I will read from a stringstream, but reading from a file is the same
std::istringstream iss{ R"(1 2 3 4
5 6 7 8
9 0 1 2
)" };
int main() {
Matrix matrix{};
// Read and parse the complete matrix
iss >> matrix;
// Debug output. Show matrix
std::cout << matrix;
}
And with that you could even read assymetric matrices, so matrices, with a different number of columns in different rows.
Hope this helps . . .