Classes for output operations


Extensions to the ANSI/ISO standard may be available allowing us to read from and/or write to file descriptors. However, such extensions are not standard, and may thus vary or be unavailable across compilers and/or compiler versions. On the other hand, a file descriptor can be considered a device. So it seems natural to use the class streambuf as the starting point for constructing classes interfacing file descriptors.

In this section we will construct classes which may be used to write to a device identified by a file descriptor: it may be a file, but it could also be a pipe or socket. Section 20.1.2 discusses reading from devices given their file descriptors, while section 20.3.1 reconsiders redirection, discussed earlier in section 5.8.3.

Basically, deriving a class for output operations is simple. The only member function that must be overridden is the virtual member int overflow(int c) . This member is responsible for writing characters to the device once the class's buffer is full. If fd is a file descriptor to which information may be written, and if we decide against using a buffer then the member overflow() can simply be:

class UnbufferedFD: public std::streambuf

{

public:

int overflow(int c)

{

if (c != EOF)

{

if (write(fd, &c, 1) != 1)

return EOF;

}

return c;

}

...

}

The argument received by overflow() is either written as a value of type char to the file descriptor, or EOF is returned.

This simple function does not use an output buffer. As the use of a buffer is strongly advised (see also the next section), the construction of a class using an output buffer will be discussed next in somewhat greater detail.

When an output buffer is used, the overflow() member will be a bit more complex, as it is now only called when the buffer is full. Once the buffer is full, we first have to flush the buffer, for which the (virtual) function streambuf::sync() is available. Since sync() is a virtual function, classes derived from std::streambuf may redefine sync() to flush a buffer std::streambuf itself doesn't know about.

Overriding sync() and using it in overflow() is not all that must be done: eventually we might have less information than fits into the buffer. So, at the end of the lifetime of our special streambuf object, its buffer might only be partially full. Therefore, we must make sure that the buffer is flushed once our object goes out of scope. This is of course very simple: sync() should be called by the destructor as well.

Now that we've considered the consequences of using an output buffer, we're almost ready to construct our derived class. We will add a couple of additional features, though.

  • First, we should allow the user of the class to specify the size of the output buffer.
  • Second, it should be possible to construct an object of our class before the file descriptor is actually known. Later, in section 20.3 we'll encounter a situation where this feature will be used.

In order to save some space, the successful operation of the various functions were not checked. In `real life' implementations these checks should of course not be omitted. Our class ofdnstreambuf has the following characteristics:

  • The class itself is derived from std::streambuf:

· class ofdnstreambuf: public std::streambuf

  • It uses three data members, keeping track of the size of the buffer, the file descriptor and the buffer itself:

· unsigned d_bufsize;

· int d_fd;

· char *d_buffer;

  • Its default constructor merely initializes the buffer to 0. Slightly more interesting is its constructor expecting a filedescriptor and a buffer size: it simply passes its arguments on to the class's open() member (see below). Here are the constructors:

· ofdnstreambuf()

· :

· d_bufsize(0),

· d_buffer(0)

· {}

· ofdnstreambuf(int fd, unsigned bufsize = 1)

· {

· open(fd, bufsize);

· }

  • The destructor calls the overridden function sync(), writing any characters stored in the output buffer to the device. If there's no buffer, the destructor needs to perform no actions:

· ~ofdnstreambuf()

· {

· if (d_buffer)

· {

· sync();

· delete[] d_buffer;

· }

· }

  • The open() member initializes the buffer. Using setp(), the begin and end points of the buffer are set. This is used by the streambuf base class to initialize pbase() pptr() and epptr():

· void open(int fd, unsigned bufsize = 1)

· {

· d_fd = fd;

· d_bufsize = bufsize == 0 ? 1 : bufsize;

·

· d_buffer = new char[d_bufsize];

· setp(d_buffer, d_buffer + d_bufsize);

· }

  • The member sync() will write any not yet flushed characters in the buffer to the device. Next, the buffer is reinitialized using setp(). Note that sync() returns 0 after a successful flush operation:

· int sync()

· {

· if (pptr() > pbase())

· {

· write(d_fd, d_buffer, pptr() - pbase());

· setp(d_buffer, d_buffer + d_bufsize);

· }

· return 0;

· }

  • Finally, the member overflow() is overridden. Since this member is called from the streambuf base class when the buffer is full, sync() is called first to flush the filled up buffer to the device. As this recreates an empty buffer, the character c which could not be written to the buffer by the streambuf base class is now entered into the buffer using the member functions pptr() and pbump(). Notice that entering a character into the buffer is realized using available streambuf member functions, rather than doing it `by hand', which might invalidate streambuf's internal bookkeeping:

· int overflow(int c)

· {

· sync();

· if (c != EOF)

· {

· *pptr() = c;

· pbump(1);

· }

· return c;

· }

  • The member function implementations use low-level functions to operate on the file descriptors. So apart from streambuf the header file unistd.h must have been read by the compiler before the implementations of the member functions can be compiled.

Depending on the number of arguments, the following program uses the ofdstreambuf class to copy its standard input to file descriptor STDOUT_FILENO, which is the symbolic name of the file descriptor used for the standard output. Here is the program:

#include

#include

#include

#include "fdout.h"

using namespace std;

int main(int argc)

{

ofdnstreambuf fds(STDOUT_FILENO, 500);

ostream os(&fds);

switch (argc)

{

case 1:

os << "COPYING cin LINE BY LINE\n";

for (string s; getline(cin, s); )

os <<>

break;

case 2:

os << "COPYING cin BY EXTRACTING TO os.rdbuf()\n";

cin >> os.rdbuf(); // Alternatively, use: cin >> &fds;

break;

case 3:

os << "COPYING cin BY INSERTING cin.rdbuf() into os\n";

os <<>

break;

}

}

No comments: