Hello again! In this video, we are going to look at stringstreams. The C++ library gives us several
different types of streams.
The most basic one is ios, which we are probably not going to use directly.
iostream is for input and output operations. fstream is for file operations.
And now we have stringstream for string operations.
So how does that work?
There is an input and an output string stream. They are defined in the sstream header.
They have a data member, which is a standard string, and they have the same member functions as the other
stream classes.
So these are really wrappers around the string class, to make it look like a stream object.
If you are familiar with Design Patterns, you probably recognize that as an example
of the adapter pattern. That is where you have a class which has an interface which makes it look and
behave like a different class.
In this case, we have a string class which has an interface to make it look and behave like a stream
object.
For the output string stream, this has an empty string object. And then we can use stream operations
to store data in that string.
So, for example, we can use the left shift operator to add the data to the string. To get at
this data we call the str() member function, and that will return a copy of the stream's string.
One thing we could do with an output string[stream] is to implement our own version of the library to_string function.
You will remember, when we looked at converting numbers to strings, that this function will take an int
or a double and return a string which has characters which correspond to the digits of that number.
We can now implement our own version.
I am going to give it a slightly different name to make sure it does not
get confused with the library function.
This is going to take an int, and it will return the string with the characters.
We create an output stringstream object in the function body, and then we send the argument to the stream.
So that is just like sending an integer to the screen or to a file.
The left shift operator is going to do this actual conversion of converting the bits in this int into
the characters for the digits.
And instead of sending it off to the monitor or into a file, it is going to store it in its internal
string.
And then we call the str() member function to get a copy of the string and we return it.
So if we call this function with an argument of 42, this will return the string with the characters
'4' and '2'.
And this does not just work for int, it will work for doubles.
And, in fact, it will work for any type where you can use the left shift operator to send the data from
the object to a stream.
So we can, in fact, write a generic version of this. This will work with any type that supports
that.
So it's going to be a template class.
The templates parameter is capital 'T'.
When we call this function, the compiler will look at the function call, and work out what type T
needs to be for the call to work.
Then it will generate a new function in which T is replaced by that type.
We pass an object of that type by const reference, because that is how we pass objects.
The body of the function is exactly the same.
We have an output stringstream.
We send the argument to it, and we return the stream's string.
So let's have a look at this.
There is the template function.
The main function is exactly the same as for the to_string example, except we are using our own version.
The argument is a double.
It's a floating point literal, which will have type double by default.
So the compiler will see that T needs to be a double.
It will generate the code for a function, with T replaced by double.
And then that will just work like a normal function call.
So this is going to convert the double 3.14159 into a string with characters
'3', '.', '1', '4', '1', '5', '9'. And then we append that to another string.
So there we are.
We get "Hello, 3.14159".
Another thing we can do with an output string is to build up input. And another design pattern
there, the builder pattern.
So you can use this to build up a output string as the data becomes available, or if it is more convenient
than doing it all in a single operation.
For example, if you have input and output mixed up and/or you are formatting the output, then it is
usually more convenient to use a string stream. As in this example, where we read words in from
the text, and then we format the word and print it out.
So we could use a separate variable for each word and then have one long output statement with all
the formatting.
But it is much neater just to do it just like this.
So we read one word. And then we format it, and send it to the output string stream.
Then we read the next word, format that, and send it to the string.
So that is going to append the data to the end of the string, inside the output stream buffer.
It is just like sending data to the screen. So the data that you send is going to be printed after
the data that was sent before. Or if you are writing to file, the data is going to be added to
the file, after the data that you sent earlier on.
And then finally, we call the the member function to get the string and print out the result.
So here is that code.
So let's try it out.
Please enter a word. "Hello".
Please enter another word. "Goodbye".
And there we are, we get "Hello" and "Goodbye".
And they are both formatted with fixed field widths. Incidentally,
One thing I meant to show you in the video on formatting and did not.
If you have a fixed field width and there is too much data to fit in that field.
So here we have 12 characters.
So if I enter more than... 12 characters.
So what will happen here is that the string is not going to attempt to format the data, because it is
too big.
Instead, it will just print it as it is.
So there we are.
We get all the characters that we entered.
So the alternative would be to truncate the data to make it fit, which is not so good.
If you have a choice between badly formatted data and bad data, then give me the bad formatting any
day!
With an input stringstream, we give it a copy of a string, which has some data. So we pass that
to the constructor of the istringstream.
Then we can read the data from the string and we can do this using any other operation that we would use
for an input stream, such as the right shift operator.
So this is useful for processing input.
If the user enters a line of text, we can break it up into words or numbers. Or reading input from a
file.
It is also useful for parsing things like source code or XML or JSON.
The way you would use this with input is that you would read the input into a string object, and then
you can check the input data.
You make sure it is in the right format.
All the data is there.
It is all sensible and so on.
And if there is a problem, you can handle the error.
And then if everything is fine, then you can pass that string object to an input string stream, and
then you can read the data from the input string stream just like you would from cin.
But this time you know there is not going to be any problem, because you have already validated the
data.
So the advantage of this is that you have separated the actual parsing of the data from the input, from the
reading, and validation.
So this allows you to put in this extra step here, if you want it.
So the way you would use this is you would call getline(), so that will read a line of input from the keyboard
into the string object.
Then you can do your validation.
Then you pass the line of input to an input stringstream. And then you can just get the data from the
input string stream.
So that's just like using cin, and you can loop over this until you get to the end of the input.
So here is an example. I have a data file.
Let's have a look at that...
So there is our data file.
It is rather strangely formatted.
Maybe there is meant to be 4 numbers to a line. Maybe there is meant to be five, but something there is not quite
right.
So we open this file. And then we read a line from the file into this string. And then we could have
a validation step where we check.
And if the format is not correct, then we can fix it up, or handle an error, or raise an exception, or whatever
we want to do.
Then we pass the input to the stringstream. And then we get the numbers from the stringstream.
We are going to add these numbers to a vector. So each time we read a number, we push it back onto the
vector.
And then we go through the vector. We have a range-for loop and we print out each element in the vector.
We also want to calculate the average of these numbers, so we are going to add them together.
We have a sum variable, which initially is zero. Then we add each element to it.
So when we finish the range, this will have the total of all the elements. And then we divide that by
the number of elements, which we get from the size() member function.
And that should print out the average.
So there we are.
We have our numbers and we have our average.
I hope that is correct.
So what are the practical applications of string streams? Output stringstreams are useful when you are
interacting with systems which expect to get a string, which has a particular format. So sometimes you
need to have a string which has control characters in it, or it has fields which have certain
widths. And that could be for a GUI or an operating system or some third party library.
And with ostringstreams, it is much easier to build those up than it is with string operations.
For i stringstreams, we can use these with getline to process input, either from the keyboard
or from a file. With a istringstream, we can use that with getline() to process input and that can be
from the keyboard or from a file.
And that is much easier than using the right shift operator directly.
Okay, so that is it for this video.
I'll see you next time.
But meanwhile, keep coding!