P.J. Plauger is senior editor of C/C++ Users Journal, He is convener of the ISO C standards committee, WG14, and active on the C++ committee, WG21. His latest books are The Draft Standard C++ Library, and Programming on Purpose (three volumes), all published by Prentice-Hall. You can reach him at pjp@plauger.com.
Special Notice C++ Draft Public Review
The US public review of the draft C++ standard is scheduled to begin as early as April 1995. For detailed information on how to get a copy of the draft and how to submit comments, send a message to the reflector c++std-notify@research.att.com. pjp
Introduction
The header <strstream> defines three classes that cooperate to help you read and write character sequences stored in memory (a.k.a. "string buffers"):
An in-memory character sequence is, in many ways, the simplest kind of stream buffer. The buffer maintained by the controlling streambuf object is not merely a window on some separate representation, like an external file. Rather, the buffer is the character sequence in its entirety.
- strstreambuf, derived from streambuf to mediate access to an in-memory character sequence and grow it on demand
- istrstream, derived from istream to construct a strstreambuf object with an input stream and to assist in extracting from the stream
- ostrstream, derived from ostream to construct a strstreambuf object with both input and output streams and to assist in inserting into the stream
Nevertheless, a strstreambuf object can control a variety of in-memory character sequences:
All these choices are reflected in a plethora of constructors for the three classes defined in <strstream>.
- an array of characters all of whose elements are defined from the outset
- an array of characters whose initial elements contain a null-terminated string
- an initially empty array of characters that can grow on demand, with storage allocated by new expressions and freed by delete expressions
- an initially empty array of characters that can grow on demand, with storage allocated and freed by functions supplied by the program
The Simple Choices
Despite all the options, three simple choices stand out. You can initialize an istrstream object to control a constant array of characters. You can then extract from its associated input stream, but you cannot insert to the output stream. The result is effectively an istream object where you dictate the stream contents purely within the program. Or you can initialize an ostrstream object to control a non-constant array of characters. You can then insert into its associated output stream to store into the array.Your third simple choice is to initialize an ostrstream object to control an initially empty dynamic output stream that can grow on demand. The result is effectively an ostream object where you capture the stream contents purely within the program. You can then capture the final stream contents before the ostrstream object is destroyed. Class strstreambuf supplies several member functions to help you capture these contents. For an object x of class strstreambuf, here's what you can do:
Here is the obvious use for the manipulator ends, by the way. (See "Standard C/C++: The Header <ostream>," CUJ, September 1994.) It's a clear way to supply a null character at the end of an in-memory character sequence.
- Call x.str() to obtain a pointer to the start of the controlled character sequence. The function freezes the racter sequence to prevent further insertions. Freezing also prevents the destructor for x from freeing the character sequence you assume that responsibility when you obtain the pointer.
- Call x.freeze() to freeze the character sequence, or x. freeze(0) to unfreeze it. The latter call is particularly helpful if you've learned what you need from an earlier x.str() call and now want the destractor to free the character sequence for you.
- Call x. pcount() to count the number of characters inserted in the sequence. The count is useful information for arbitrary character values, or if you don't insert a null character at the end of the character sequence.
I end this brief introduction by mentioning the ghost at the banquet table. The Standard C library has long supported a similar capability. You can obtain formatted input from an in-memory character sequence by calling sscanf. You can write formatted output to an in-memory character sequence by calling sprintf or vsprintf. (All three functions are declared in <stdio.h>.) So how are these three classes any better? The answer is easy:
I believe these are ample reasons to cultivate a knowledge of the header <strstream>.
- An istrstream object looks like any other istream object controlling an input stream. And an ostrstream object looks like any other ostream object controlling an output stream. The older functions oblige you to know that you're dealing with an in memory character sequence. And you always have to know exactly where the sequence resides.
- Unlike sscanf, an istrstream object can control a sequence of arbitrary character values. It can have embedded null characters. Equally, it doesn't need a terminating null character.
- sprintf writes to an array of fixed length whose length is nevertheless not known to the function. You avoid storage overwrites only by restricting every single conversion specification. By contrast, an ostrstream object can control a sequence of known length. Insertions fail before storage overwrite occurs. Or the object can control a dynamic sequence of arbitrary length. Insertions fail only when heap storage is exhausted.
What the Draft Standard Says
Class strstreambuf is the first of several classes derived from streambuf. (I describe more in future installments, on the headers <sstream> and <fstream>.) What makes each such stream buffer unique is the way it overrides the virtual member functions in the base class. The exception classes exhibit uniqueness in a small way, by overriding the virtual member function do_raise. (See "Standard C: The Header <exception>," CUJ, February 1994.) But the stream buffers indulge in specialization big time.The convention within the draft C++ Standard is to comment out virtual member functions in the derived class. Each such comment is labeled inherited. The suggestion is that an implementation need not provide an overriding definition if the definition in the base class does the job.
For the classes derived from streambuf, however, these comments are often misleading. Listing 1, for example, shows how the draft C++ Standard defines the class strstreambuf. Several critical virtual member functions must have overriding definitions to satisfy the semantic requirements of the class. (The descriptions of these functions in the draft C++ Standard are often hard to read as well. They represent standardese at its legalistic extreme. But those descriptions are also the definitive word on how a class behaves in peculiar circumstances. Turn to them when tutorials prove inadequate.)
Listing 2 shows how the draft C++ Standard defines the class istrstream, and Listing 3 shows how it defines the class ostrstream. These are derived from the classes istream and ostream, respectively, to facilitate operations with stream buffers of class strstreambuf.
Using the Header
You include the header <strstream) to make use of any of the classes istrstream, ostrstream, or strstreambuf. Objects of these classes let you read and write in-memory character sequences just as if they were conventional files. You can choose among four patterns of access:
I deal with each of these options in turn.
- read only
- write only
- simple read/write
- sophisticated read/write
Read-Only String Buffers
If all you want to do is read an in-memory character sequence, construct an object of class istrstream to specify the character sequence and control extractions from it. For a null-terminated string s, you can write:
istrstream strin(s);The character sequence begins at s and continues up to, but not including, the terminating null character that follows.To control an array of n characters s with arbitrary values, you can write:
istrstream strin(s, n);In either case, s can be either a pointer to char or a pointer to const char. Whichever way you construct the istrstream object, the resultant stream buffer (pointed at by strin.rdbuf()) does not support insertions. You must specify an initial character sequence class istrstream has no default constructor. The character sequence remains unchanged for the life of the istrstream object. (I suggest you refrain from altering the contents of the character sequence by other means, if that is possible, while the istrstream object controls accesses to it.)Positioning within a read-only stream is fairly simple. A streamoff value is effectively an index into the character sequence. Thus:
strin. rdbuf()->pubseekoff(0 ios::beg);sets the stream position at the beginning (position zero) of the character sequence. If the length of the sequence is nonzero, the next character extracted will be the first character in the sequence. Any attempt to set the stream position to a negative value, or beyond the end of the character sequence, will fail. You can also call streambuf::pubseekpos, as above, but that is less necessary for an in-memory character sequence. Do so for code that shouldn't need to know what flavor stream buffer it is really dealing with. (For more discussion of stream positioning, see "Standard C: The Header <streambuf>," CUJ, June 1994.)For what it's worth, you can also call strin.str() for a read-only character sequence. The call does not freeze the character sequence, since it is not dynamically alterable. The function merely returns a pointer to the beginning of the character sequence, just as for a character sequence you construct (as described below). This is always the beginning of the character sequence you specified when constructing the object.
If you do call strin.str() for a read-only character sequence as above, be warned. The call strin.rdbuf()->pcount() will not return the length of the sequence. Since no output stream exists, this call returns zero. You must determine the length of the sequence by some other means, such as positioning the stream at the end and inspecting the resultant stream offset.
Write-Only String Buffers
If all you want to do is create an in-memory character sequence, construct an object of class ostrstream to control insertions into it. You can write:
ostrstream strout;then insert into strout just like any other output stream. The character sequence can grow dynamically to arbitrary length. (The actual limit is usually INT_MAX, defined in <limits.h>, or when a storage allocation request fails.)If you want a null-terminated string, be sure to insert a null character last, as with:
strout << ends;It will not be supplied for you when you call strout.str(). For a sequence of arbitrary characters, you can determine the number of characters you inserted by calling strout.pcount(). But see the warning below.)Positioning within a write-only stream is also fairly simple. You can work mostly with streamoff values, as for read-only streams described above. A few caveats are in order, however:
As usual, my advice is to avoid such delicate areas in code you hope to keep portable. If you insist on pushing the limits of clearly defined behavior, expect surprises.
- Current practice seems to vary on where you can set the stream position. The safest bet is to move only to character positions with values already defined either at construction or as a result of earlier insertions.
- Some confusion is also endemic about the interaction of stream positioning requests and the value returned by a call such as strout.pcount().
- Even an implementation that obeys the draft C++ Standard can still surprise. Strictly speaking, ostrstream::pcount calls strstreambuf::pcount, which does not return a count of all insertions. Rather, it returns the difference between the current output stream position and the initial output stream position. The former can be left other than at the end of the character sequence by stream-positioning requests. The latter can be set beyond the beginning of the character sequence by a more sophisticated strstreambuf constructor, as described below.
Your goal in creating a write-only character sequence is to capture the final result, as a rule. Call strout.str() to freeze the character sequence and return a pointer to its initial character. Remember that freezing the character sequence tells the strstreambuf destructor not to free storage for the character sequence. You must either free the storage yourself or be sure to call strout.freeze(0) before strout is destroyed.
Simple Read/Write String Buffers
If you want to create an in-memory character sequence that you can read as well as write, you need two objects to control the input and output streams. First, construct an object of class ostrstream to supply the strstreambuf object and control insertions, then construct an object of class istream to control extractions. You can write:
ostrstream strout; istream strin(strout. rdbuf());much as I have described several times in earlier installments for setting up a bidirectional stream. (See, for example, "Standard C: Introduction to Iostreams," CUJ, April 1994.) You can then insert into strout just like any other output stream. And once you have inserted characters, you can extract them from strin just like any other input stream.A few caveats are in order, however. Try to extract beyond the last character in the sequence and you will encounter end-of-file. Once you extend the sequence by inserting characters, you can successfully extract again, but not until you clear eofbit if it is set in the istream object. Once again, this status bit proves less than trustworthy.
You can position the input and output streams separately or jointly. As usual, positioning operations demand a modicum of caution:
Note that the second caveat applies even to read-only or write-only in-memory character sequences.
- The draft C++ Standard specifies a default third argument value which = ios::in | ios::out for streambuf::pubseekoff. (It specifies the same for the default second argument to streambuf::pubseekpos.) Thus, unless you supply an actual which argument that selects only one stream ios::in or ios::out a call to this member function endeavors to position both streams in tandem. Seldom does that make sense. When both reading and writing an in-memory character sequence, always specify the which argument on such calls.
- The draft C++ Standard is even nastier at times. If the second argument to streambuf::pubseekof is ios::cur, a tandem positioning operation will fail. For such relative positioning requests, you should always specify a which argument that selects only one stream.
As an important aside, here is a warning regarding the member function rdbuf. Several classes in the Standard C++ library are derived from either istream or ostream. These include istrstream and ostrstream, in this particular header. All such derived classes also provide a member function rdbuf() that hides the member function in the base class ios.
Why is this so? All such derived classes also provide a member object of a class derived from streambuf. In this header, that derived class happens to be strstreambuf. Consider, for example, the call istr.rdbuf(), for an object istr of class istrstream. It returns a pointer to the strstreambuf member object in istr. And the return type is indeed pointer to strstreambuf, not pointer to streambuf as in the base class ios.
A generic pointer to the base streambuf gets you to all the inherited member functions. It even gets you to the proper overriding definitions of virtual member functions. But to access any member functions peculiar to the derived class, you need such a specialized pointer. Again in this particular case, the member functions freeze, pcount, and str are peculiar to class strstreambuf, not its base class.
Potential confusion arises, however, once you make a call such as istr.rdbuf(strout.rdbuf()). The critical pointer in the ios subobject now designates a different stream buffer. But a subsequent call to istr.rdbuf() still returns a pointer to the member object within istr. To get the pointer actually used by inserters and extractors, you must make the hairy call ((ios&)istr).rdbuf().
Got all that? If not, don't worry too much about it. The bottom line is that you should follow a fundamental style rule. Alter the stored stream buffer pointer only in an object of class istream or ostream. Never do so in an object of a class derived from one of these. Even better, do as I did above use a special istream or ostream object constructed from the outset to share an existing stream buffer. With either approach, there's never a second stream buffer lying around to cause confusion.
Sophisticated String Buffers
You can get even fancier with the classes defined in <strstream>. What follows is an assortment of more sophisticated setups.Say you want to use an existing character array to store a character sequence, and you want to read the character sequence once you write it. If you declare:
ostrstream strout(s, n); istream strin(strout.rdbuf());then strout controls the sequence of n characters beginning at s. Both the read position and the write position are at the beginning of the character sequence. In this case, you cannot extend the sequence by inserting at or beyond character position n.Here's a variation on this scenario. Say you want to use an existing character array to store a character sequence, and the array already stores a null-terminated string. You want to begin extracting from the beginning of the character sequence, but you want to begin inserting at the end of the null-terminated string (first replacing the terminating null). If you declare:
ostrstream strout(s, n, ios::app); istream strin(strout.rdbuf());then you get the desired behavior. Insertions effectively append characters to an initial null-terminated string, but won't continue past the end of the character array. For extractions, see the caveat above about the behavior of eofbit.If you want to get fancier, you have to construct a strstreambuf object directly. This class has lots of constructors with lots of options. I describe here just the two that I feel are genuinely useful and safe.
Say you want to construct an in-memory character sequence that you know will get very large. You'd like to suggest, when constructing the strstreambuf object, that the object allocate space for a large sequence right off the mark. That can save lots of execution overhead in reallocating storage as the character sequence grows. Equally, you might want to construct a number of character sequences all of which will be fairly small. You'd like to suggest that each such object allocate only a small number of characters. That can save lots of unused storage. If you declare:
strstreambuf sb(n); istream strin(&sb); ostream strout(&sb);for some int value n, the constructor should take the hint. What it does with it is up to the implementation, but at least you get to make the suggestion.Another approach to storage allocation is to do the job yourself. So your final interesting choice is to begin with two functions like:
#include <stdlib. h> void *my_alloc(size_t n) { // allocate n chars return (malloc(n)); } void my_free(void *p) { // free allocated storage free(p); }Tailor these basic functions as you see fit. Then you can declare:
strstreambuf sb(&my_alloc, &my_free); istream strin(&sb); ostream strout(&sb);The two functions you supply will be called, in place of new and delete expressions, to allocate and free storage for character sequences.
Next Month
That's a capsule summary of how to use string buffers, as specified by the draft C++ Standard. Next month, I'll show you one way to implement this header, and discuss some of the interesting challenges it presents.This article is excerpted in part from P.J. Plauger, The Draft Standard C++ Library, (Englewood Cliffs, N.J.: Prentice-Hall, 1995.)