Using istringstream in C++

Drake Daniels picture Drake Daniels · Nov 27, 2018 · Viewed 12.5k times · Source

I have some code that utilizes fork, execlp, and wait to make two processes. The objective is to be able to repeatedly print a prompt and have the user enter a command with up to 4 arguments/options to the command.

int main()
{
     string command, argument;
     istringstream iss(argument);

  do
  {
  //prompt user for command to run
  cout << "Enter command: ";
  cin >> command >> argument;

  int pid, rs, status;
 
  //fork will make 2 processes
  pid = fork();
  if(pid == -1) { perror("fork"); exit(EXIT_FAILURE); }

if(pid == 0) {
//Child process: exec to command with argument

//C_str to get the character string of a string
rs = execlp(command.c_str(), command.c_str(), argument.c_str(), (char*) NULL);
.
if (rs == -1) { perror("execlp"); exit(EXIT_FAILURE); }
} else {
   //Parent process: wait for child to end
   wait(&status);
      }
   } while(command != "exit");

   return 0;
}

I knew that the current code I have would be able to support only one argument to the command, but I wasn't sure about what to use in order to specify between 1 to 4 arguments. That's when my friend mentioned about std::istringstream, but while looking into it, I didn't understand how to use it for input with the rest of the program. Is there a way to set it up or is there a different method to use to fulfil the requirements?

Answer

paddy picture paddy · Nov 27, 2018

The most common usage pattern for std::istringstream with user input is to accept a single line of text and then process it. This avoids problems that can occur when the input does not match what you expect, or cannot be predicted.

Here is a simple example that reads one line at a time from STDIN, and processes it into a command followed by a vector of strings as arguments.

for(std::string line; std::getline(std::cin, line); )
{
    std::istringstream iss(line);

    std::string command;
    if (iss >> command)
    {
        std::vector<std::string> args;
        for (std::string arg; iss >> arg; )
        {
            args.push_back(arg);
        }
        std::cout << "Received '" << command << "' with " << args.size() << " arguments\n";
    }
    else
    {
        std::cerr << "Invalid input" << std::endl;
    }
}

Of course, you don't need to read into strings, or store stuff in a vector. This was simply for illustrative purposes.

The basic point is to avoid pitfalls that people come up against, the most common of which is expecting the previous stream operation to have succeeded. When that assumption is false, the naive programmer can find themselves attempting to parse something that was supposed to be handed in the previous line.


Broken example:

#include <iostream>

int main() {
    std::string command;
    while (std::cin >> command)
    {
        std::cout << "Got command: " << command << std::endl;
        if (command == "foo")
        {
            // 'foo' expects two integer arguments:
            int arg1, arg2;
            std::cin >> arg1 >> arg2;
            std::cout << command << ": " << arg1 << ", " << arg2 << std::endl;
        }
        else if (command == "bar")
        {
            // 'bar' expects one float argument:
            float arg1;
            std::cin >> arg1;
            std::cout << command << ": " << arg1 << std::endl;
        }
    }
    return 0;
}

In the above, let's say the user gets confused and "invokes" the foo command with one float argument, then the next command is a valid bar command:

foo 42.0
bar 3.14

Here's what happens:

  1. The foo handler reads arg1 as 42, then fails to read the next argument. The stream is now in error.

  2. No error checking was done on the input during that handler, so there's now undefined behavior outputting the value of arg2.

  3. When attempting to read the next command, the stream is already in an error state and so the loop terminates.

So, the output of that program might look like:

Got command: foo
foo: 42, -149017896

Fixing this problem without istringstream is possible, but it's a pain. There are many reasons why a stream might enter an error state, and clearing error flags for specific error states just to get around this problem makes for ugly and potentially error-prone code. Not only would you need to clear the error flags, but you would also need to tell the stream to ignore any remaining characters in the line. And you may have already started reading into the next line without knowing.


More robust example:

#include <iostream>
#include <sstream>

int main() {
    std::string command;
    for (std::string line; std::getline(std::cin, line); )
    {
        std::istringstream iss(line);
        if (!(iss >> command))
            continue;
        std::cout << "Got command: " << command << std::endl;
        if (command == "foo")
        {
            // 'foo' expects two integer arguments:
            int arg1, arg2;
            if (iss >> arg1 >> arg2)
            {
                std::cout << command << ": " << arg1 << ", " << arg2 << std::endl;
            }
        }
        else if (command == "bar")
        {
            // 'bar' expects one float argument:
            float arg1;
            if (iss >> arg1)
            {
                std::cout << command << ": " << arg1 << std::endl;
            }
        }
    }
    return 0;
}

Now, the same input from the previous example will give the output:

Got command: foo
Got command: bar
bar: 3.14

The differences are:

  • Using istringstream, any errors processing our input did not affect the source stream, so a failure on the previous line does not cause problems.

  • The string stream is correctly checked when reading arguments, allowing optional error handling.

  • If, after some parsing failure, you decide to try processing a line of input differently, that's easy to do with another string stream.