0

I'm working a small project for one of my classes that involves taking formatted input from a file, and I'm having trouble with detecting newlines. Here's the function I'm working with:

void averager(vector<double> &outvec)
{
    ifstream temp;
    vector<int> tempvec;
    double average;

    temp.open("temp.dat");

    int num;

    while (! temp.eof())
    {
        temp >> num;

        if (num != 9999)
            tempvec.push_back(num);

        else if (num == 9999)
        {
           average = accumulate(tempvec.begin(), tempvec.end(), 0.0) / tempvec.size();

           outvec.push_back(average);
           tempvec.clear();
        }
    }

    temp.close();

    return;
}

I'm taking input from a temporary file that contains sets of integers, test scores in this case, separated by spaces, ending with the integer 9999 as a delimiter. When it hits the delimiter, it stops to average the integers, puts the new number into the vector outvec, and starts again on the next line.

I would like to use just a '\n' character as the delimiter, but it doesn't seem like I am able to check if my num variable is equal to a character, preferably a newline. My method of injecting a weird integer at the end of the line works, but I don't like it.

Sorry if this code is horrible or if this question is is stupid; I'm a beginner and I want this to be good code. Thanks in advance!

kiwi_
  • 15
  • 2

2 Answers2

0

The function std::getline() can be used for this. It reads a line of input into a string. With that string you can then parse out all the integers.

To parse the integers you can use a std::istringstream, which acts just like std::cin but it uses a string as the source instead of the console.

#include <sstream>
// ...
std::string line;
while (std::getline(std::cin, line)) {
  std::istringstream sstream(line);
  int temp;
  while (sstream >> temp) {
     tempvec.push_back(temp);
  }
  average = accumulate(tempvec.begin(), tempvec.end(), 0.0) / tempvec.size();
  outvec.push_back(average);
  tempvec.clear();
}
David G
  • 94,763
  • 41
  • 167
  • 253
  • Hey there, I'm getting an error when ```std::istringstream sstream(line);``` is called, telling me "Incomplete type is not allowed". Any insight? – kiwi_ Mar 29 '20 at 02:27
  • @kiwi_ `std::istringstream` is provided under the header ``, so you'll need to include that with `#include `. – David G Mar 29 '20 at 02:28
  • That worked perfectly! I just wasn't giving getline the right data. Thanks a bunch! – kiwi_ Mar 29 '20 at 02:38
0

First of all. The anser is given, very good and adequate for the level of experience and accepted. So all good.

I would like to give an additional answer to show you, how a more modern C++ solution using C++17 and how it would look like. The basic algorithm is the same as in the give answer.

First, please see the code:

std::vector<double> averager(const std::string& fileName) {

    // The resulting data. A vector with all average values per line
    std::vector<double> result{};

    // Open the file and check, if it could be opened.
    if (std::ifstream sourceFileStream(fileName); sourceFileStream) {

        // Now read all lines of the file in a simple for loop
        for (std::string line{}; std::getline(sourceFileStream, line); ) {

            // Put the just read line into an std::istringstream for easier extraction
            std::istringstream iss{line};

            // Construct a std::vector value using its range constructor and get all ints in the iss
            std::vector values(std::istream_iterator<int>(iss), {});

            // Calculate the average and store it in our result vector
            result.emplace_back(std::accumulate(values.begin(), values.end(), 0.0) / values.size());
        }
    }
    else {
        std::cerr << "\n*** Error: Could not open source file '" << fileName << "'\n";
    }
    return result;
}

const std::string fileName{"r:\\temp.dat"};

int main() {

    // Read all averages from file
    std::vector avr{ averager(fileName) };

    // Show the result to the user
    std::copy(avr.begin(), avr.end(), std::ostream_iterator<double>(std::cout, "\n"));

    return 0;
}

Ok, then lets have a look at it. At first we see many comments in it. It is very important to write comments. That helps you to understand waht your are doing. Now and later. It even will prevent errors. And, the quality of the code increases. The quality of code without comments is ZERO.

What you see next is that I change the signature of the function. I will return the result. In earlier times, people thought that it is not good to return big or complex data. But in C++ we have now RVO (return value optimization) and copy elision.

With RVO and Copy elision you can and should return "by-value". Even for super big objects. Please see also here and here

Next, the "if-statement".

we have here a if statement with initializer. This is available since C++17. You can (in addition to the condition) define a variable and initalize it. So, in

if (std::ifstream sourceFileStream(fileName); sourceFileStream) {

we first define a variable with name "sourceFileStream" of type std::ifstream. We use the uniform initialzer "{}", to initialze it with the input file name.

This will call the constructor for the variable "sourceFileStream", and open the file. (This is, was this constructor does). After the closing "}" of the "if-statement" the variable "sourceFileStream" will fall out of scope and the destructor for the std::ifstream will be called. This will close the file automatically.

This type of if-statement has been introduced to help to prevent name space pollution. The variable shall only be visible in the scope, where it is used. Without that, you would have to define the std::ifstream outside (before) the if and it would be visible for the outer context and the file would be closed at a very late time. So, please get aquainted to that.

Next and similar, the while statement. We do not declare a variable in an out scope, then start a while loop, where we do use the declared variable just in the inner scope of the while. You may have heard the for and while are exchangable, because of identical functionality. But with for you have the possibility to have an initializer as the first element. Hence, for that reason, the usage of for is the more-recommended solution at this time.

So the loop will look like

// Now read all lines of the file in a simple for loop
for (std::string line{}; std::getline(sourceFileStream, line); ) {

This is completely the same as

// Now read all lines of the file in a simple for loop
std::string line{};
while (std::getline(sourceFileStream, line)) {

but, without namespace pollution and the for is only one line.

Let's have a deeper look at the condition-part of the for-statement (or while). We expect normally some boolean condition or boolean vale or a boolean result of a function. Here, this works because the std::getline returns the stream on which is was working, so a reference to "". And the stream has on overwritten boolean operator !, to check the condition of the stream. Please see here. So, if an error occurs (or, "end of file"), the condition will be false.

You should always and for every IO-Operation check, if it worked or not.

So, with the for we read the source file line by line. And each line, we will put in a std::istringstream, to be able to extract values from it like with an standard ">>"-statement (extractor).

Next line is:

// Construct a std::vector "value" using its range constructor and get all ints in the iss
std::vector values(std::istream_iterator<int>(iss), {});

UhOh. Whats that. Let's start with the std::istream_iterator. If you read the linked description, then you will find out, that it will basically call the extractor operator >> for the specified type. And since it is an iterator, it will call it again and again, if the iterator is incremented. Ok, understandable, but then . . .

We define variable values as std::vector and call its constructor with 2 arguments. That constructor is the the so called range constructor of the std::vector. Please see the descrition for constructor (5). Aha, it gets a "begin()" iterator and an "end()" iterator. OK, but what is this strange {} instead of the "end()"-iterator. This is the default initializer (please see here and here. And if we look at the description of the std::istream_iterator we can see the the default is the end iterator.

Please additionally note: Since we are using C++17, we can define the std::vector for "values" without template argument. So, not std::vector<int> values, but simply std::vector values, without the <int>. The compiler can deduce the argument from the given function parameters. This feature is called CTAD ("class template argument deduction"). we will use this consequently, also in function main later.

Aha, that's how its working.

The last important statement of the function is:

// Calculate the average and store it in our result vector
result.emplace_back(std::accumulate(values.begin(), values.end(), 0.0) / values.size());

We do not create a temporary value first and then copy its data into the vector. We use the std::veoctors emplace_back() function, to do an inplace construction of the value. This save unnecessary copy operations.

At the end, we return the result and that's it for the function.

In main, we define the variable avr (again using CTAD) and initalize it with the result of our function. All data will be read and calculated, with that simple line.

And last but not least, we copy all data to ```std::cout```` using the std::ostream_iterator.


So, I hope I could explain you a little, what you could do with modern C++

A M
  • 14,694
  • 5
  • 19
  • 44