You are asking here how you should approach the given problem.
In normal software development we do several steps. First, we analyze the requirements, then think of a design and after that, we start coding. And finally, we verify and qualify the program. In reality there are more processes. But please take one important recommendation:
Before writing code. We should first clarify the “What” and then the “how”.
Last, but not least, when doing the coding, we should follow standard patterns to avoid the most common problems.
So, now let us look at your problem. You want to read a text file. The text file contains lines with comma separated values. The lines, with the comma separated values shall be split.
Then, there are always 2 lines that belong together. The first line contains index as a integer number and the question as a string, the second line consists of also an integer index and then an integer number denoting the answer.
All the data shall be read and stored for further processing.
At this point in time, we have done the requirement analysis in general.
Next is the “How”, the design, the “how we want to do things”
You were mentioning that you want to use 2 different vectors to store the questions and the answers. That approach is basically not that good.
Because the general rule is that you should store values that belong somehow together, even having different types, like int and string, in a “struct” or “class”. The same is valid for data in the first and then in the next line.
On the other hand, many data having the same type, should store in a container, like a std::array
or std::vector
(or others, depending of the use case).
And in your case, you would have a combination of both. So, first the data with different types in a struct
and then a std::vector
of these structs.
Example for the above (one of many possible solutions):
#include <iostream>
#include <vector>
struct Question {
int index{};
std::string text{};
};
struct Answer {
int index{};
int data{};
};
struct QuestionAndAnswer {
Question question{};
Answer answer{};
};
std::vector<QuestionAndAnswer> questionAndAnswer{};
OK, understood next.
We want to open a file and read it line by line. Opening a file for reading, can be done by defining a std::ifstream
and then handing a filename to its constructor. This will open the file for you. And, at the end, when the variable of type std::ifstream
falls out of scope, then destructor of the std::ifstream
will automatically close the file for you.
You should always check the CPP Reference for any kind of questions to C++ functionalities.
As a general rule, you should always check the result of any IO-operation. This can be done with if (ifstreamVaraible)
. If you look in the definition of the IO-Stream functions than you can see that many of them return again a reference to the IO-Stream, with which that have been called. Example:
// Open the file
std::ifstream sourceFileStream(“test.txt”);
// Check, if it could be opened successfully
if (sourceFileStream) {
// Read the line and get the result
std::string line{};
if (std::getline(sourceFileStream, line)) {
. . .
}
How does this work? If you look in the documentation of the stream functions, then you will see that their bool
and not operator !
are overwritten and return the status of a stream. For the above example if (sourceFileStream) {
the compiler sees a stream variable in an if
-statement, were it expects a boolean expression. It will then take the bool
function of the stream and evaluates it.
Same is valid for if (std::getline(sourceFileStream, line))
. This will first do the getline
- operation, which will read the line. And then getline
returns a reference to the stream. Then the if
- statement contains quasi again if (sourceFileStream)
and the bool operator will be called.
With this mechanism, you can (and should) check the result of all IO-operations.
If we want to read many lines in a loop then the rule is, to put the std::getline(sourceFileStream, line)
statement into the while statements condition part. Otherwise you will always read a line to much.
You will see from not so experienced developers often something like ‘while (!sourceFileStream.eof())’ or, similar, while (sourceFileStream)
. This is considered to be wrong. There are many many statements her on SO, explaining that in more detail.
Next, if you want to learn C++ and use better approaches, then you should make use of object-oriented programming. The first step is, to put data and methods operating on this data in one class
or struct
. For your case, it would mean that the input functions should be part of the structs.
In C++ input is done via the extractor operator >>
. And therefore we should add an extractor operator to your structs.
The syntax for that is (with the example of the answer struct=:
struct Answer {
int index{};
int data{};
friend std::istream& operator >> (std::istream& is, Answer& a) {
// Read an index, a comma, and the answer
char comma{};
return is >> a.index >> comma >> a.data;
}
};
This is a function for the class
(or struct
) “Answer”, that takes a stream and an answer as input and will return again a reference to the stream. Now, we could write:
Answer answer{};
if (!(std::cin >> answer))
std::cerr << “Problem while reading answer\n”;
The if
- statement will execute the embedded extraction operation. The compiler will see a stream, the extractor operator >>
and a variable of type Answer. So, it will call the function defined in the above struct. The operation will return a reference to the stream and the !
operator of the stream, will indicate, if the stream ha and issue.
A similar mechanism can be implemented for output, so we can overwrite the inserter operator >>
as well.
With all that, we can split the above big problem into very small units, which can be implemented in an easy and understandable way.
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <iomanip>
struct Question {
// Data
int index{};
std::string text{};
// Extractor operator
friend std::istream& operator >> (std::istream& is, Question& q) {
char comma{};
// First read the index, then the comma, then eat up white space. A reference to is will be returned. getline
// will read the rest of the line by using is. It will also return is, which is then the function return value
return std::getline(is >> q.index >> comma >> std::ws, q.text);
}
// Simple output
friend std::ostream& operator << (std::ostream& os, const Question& q) {
return os << "Question: Index: " << q.index << "\tText: " << q.text;
}
};
struct Answer {
// Data
int index{};
int data{};
// Extractor operator
friend std::istream& operator >> (std::istream& is, Answer& a) {
char comma{};
// Read the index, then the comma, then data. A reference to is will be returned.
return is >> a.index >> comma >> a.data;
}
// Simple output
friend std::ostream& operator << (std::ostream& os, const Answer& a) {
return os << "Answer: Index: " << a.index << "\tData: " << a.data;
}
};
struct QuestionAndAnswer {
// Data
Question question{};
Answer answer{};
// Extractor operator
friend std::istream& operator >> (std::istream& is, QuestionAndAnswer& q) {
// Read question and answer
return is >> q.question >> q.answer;
}
// Simple output
friend std::ostream& operator << (std::ostream& os, const QuestionAndAnswer& q) {
return os << q.question << "\t\t" << q.answer;
}
};
int main() {
// Here we will store all questions and answers
std::vector<QuestionAndAnswer> questionAndAnswer{};
// Open the source file and check, if it could be opened
std::ifstream sourceFileStream("r:\\filepath.txt");
if (sourceFileStream) {
QuestionAndAnswer temp{};
// Read all questions and answers in a loop
while (sourceFileStream >> temp)
questionAndAnswer.push_back(temp);
// Show debug output
for (const QuestionAndAnswer& qa : questionAndAnswer)
std::cout << qa << '\n';
}
else
std::cerr << "\nError: Could not open source file\n";
}
Using this approach would be one of many recommended approaches for a c++ programmer.
A source file containing
1, Question 1
2, 1
3, Question 2
4, 2
5, Question 3
6, 3
7, Question 4
8, 4
9, Question 5
10, 5
will create the output:
Question: Index: 1 Text: Question 1 Answer: Index: 2 Data: 1
Question: Index: 3 Text: Question 2 Answer: Index: 4 Data: 2
Question: Index: 5 Text: Question 3 Answer: Index: 6 Data: 3
Question: Index: 7 Text: Question 4 Answer: Index: 8 Data: 4
Question: Index: 9 Text: Question 5 Answer: Index: 10 Data: 5
If you want to stick to you original idea, then you could use:
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <iomanip>
int main() {
// Here we will store all questions and answers
std::vector<std::string> questions{};
std::vector<int> answers{};
// Open the source file and check, if it could be opened
std::ifstream sourceFileStream("r:\\filepath.txt");
if (sourceFileStream) {
std::string question{};
int temp{}, answer{};
char comma{};
// Read all questions and answers in a loop
while (std::getline(sourceFileStream >> temp >> comma >> std::ws, question))
if (sourceFileStream >> temp >> comma >> answer) {
// We do not want to go out fo sync. Always store questions and answers together
questions.push_back(question);
answers.push_back(answer);
}
// Debug output
for (unsigned int k = 0; k < questions.size(); ++k)
std::cout << "Question: " << questions[k] << "\t\tAnswer: " << answers[k] << '\n';
}
else
std::cerr << "\nError: Could not open source file\n";
}
But maybe not that recommended...