To clarify, I am using the g++7.1.0 compiler and the source code for the standard library is also in this version.

This article explains the basic implementation structure of c++ standard IO, as well as the concrete implementation of cin and cout.

Before reading this article, it is recommended to take a look at the previous article, at least to understand the relationship between the various classes in standard IO:

C++ standard I/o flow relationship comb

1. Basic structure of standard I/O

By reading through the c++ standard IO source code, I summed up its underlying implementation structure, as shown in the figure:

It is divided into three layers: external devices, buffers, programs, described as follows:

  • External devices refer to physical or logical devices such as keyboards, screens and files.
  • A buffer is a block of memory that holds data before it is synchronized to an external device.
  • The program is the process that our code generates.

This is done by the ostream:: PUT function. Let’s explore the implementation of the PUT function.

1.1 First explore the bottom implementation of the bottom

Tip: TCC stands for template cc. Cc is the c++ implementation file suffix, with t indicating the implementation of the template, so TCC is a template implementation file that is used to distinguish it from other non-template implementation files.

Find the code for the put function in ostream. TCC:

template<typename _CharT, typename _Traits>
    basic_ostream<_CharT, _Traits>&
    basic_ostream<_CharT, _Traits>::
    put(char_type __c)
    {
      sentry __cerb(*this);
      if (__cerb)
	{
	  ios_base::iostate __err = ios_base::goodbit;
	  __try
	    {
	      const int_type __put = this->rdbuf() - >sputc(__c);
	      if (traits_type::eq_int_type(__put, traits_type::eof()))
		__err |= ios_base::badbit;
	    }
	  __catch(__cxxabiv1::__forced_unwind&)
	    {
	      this->_M_setstate(ios_base::badbit);		
	      __throw_exception_again;
	    }
	  __catch(...)
	    { this->_M_setstate(ios_base::badbit); }
	  if (__err)
	    this->setstate(__err);
	}
      return *this;
    }
Copy the code

To output a character, put is a spUTC member function that calls the buffer base class basic_streambuf. The SPUTC member function is implemented as follows:

int_type
      sputc(char_type __c)
      {
	int_type __ret;
	// PPTR returns a pointer to the next position in the buffer and epptr returns a pointer to the end of the buffer
	if (__builtin_expect(this->pptr()"this->epptr(), true)) {*this->pptr() = __c;
	    // Pbump increments the next position in the buffer by 1
	    this->pbump(1);
	    __ret = traits_type::to_int_type(__c);
	  }
	else
        // Overflow does buffer overflow
	  __ret = this->overflow(traits_type::to_int_type(__c));
	return __ret;
      }
Copy the code

The spUTC function has two branches:

  • One is to write characters directly to the buffer when the current position of the buffer is not full.

  • Second, if the current buffer has been written to the full, then the buffer overflow processing.

In both cases, it is clear that each output class is implemented differently. Leaving aside the basic ostream, let’s look at the similarities and differences between oStringstream and ofstream implementations.

On the first point, ostringstream and ofstream are implementation-identical in that they both write characters to the buffer and move them back one bit.

Ostringstream, however, is the overflow member function of stringbuf that calls ostringstream. It regenerates a larger temporary buffer when the original buffer runs out, copies all the data from the source buffer, and adds the current output to the new buffer. The temporary buffer is then exchanged with the source buffer, so that a character is written to the source buffer and the buffer is expanded.

Ofstream is the overflow member function of filebuf that is called. This function checks whether the current buffer has been written to the end of the buffer. Obviously, for the second point, since the buffer is full, it must have been written to the end of the buffer. It then reinitializes the buffer pointer position, etc. Note that FileBuf does not extend the buffer.

Tip: obviously, for the second point above, the overflow function is called using c++ polymorphism. For streambuf::overflow, it is a virtual function. The real implementation is in stringbuf and filebuf.

So far, we have explored the implementation of the PUT function. We have explored the basic implementation of the standard library in general, but we are still not so clear about the implementation of the three-layer structure. Let’s talk about it in detail.

1.2 Details about the standard IO infrastructure
1.2.1 Underlying structure of StringBUf

For iStringstream, oStringstream, stringstream, they all implement buffers based on stringbuf, so the underlying implementation of stringbuf is just fine. So what does Stringbuf implement buffers based on?

Let’s take a look at the following image:

Note that the arrow here represents the use relationship, not the inheritance relationship, so I have used a transparent line here, and the same goes for the rest.

So now it’s clear that stringbuf uses a string from the library as a buffer, and if we’re reading data, obviously the size of the string doesn’t change, but if we’re writing to a string, we call the construction of the string, and it starts out as an empty string, When begin to write the first character, the default will apply to the string object a dynamic memory size is 512 bytes, subsequent write, write to dynamic memory, directly after the 512 bytes written, will be based on the current memory size are superior to 2, then apply for a new memory, before the data is copied to the new memory to entirely, Then write the character to save after the new memory.

So for the three-tier structure of Stringbuf, the buffer is the requested memory, and the peripheral is the string, so logically, they’re two different skins, but in fact, implementationally speaking, we’re reading and writing to the memory requested by string, which is actually reading and writing to string, and in that sense, Stringbuf is either a three-tier structure or a two-tier structure, depending on how we interpret it, but I won’t go into that.

1.2.2 Underlying structure of FileBuf

Similarly, the underlying implementation of fstream-related classes is based on Filebuf, which is slightly more complex than Stringbuf.

When fileBuf calls open, it will create a new block of char dynamic memory. BUFSIZ is the default size of the buffer defined in the system file. When fileBuf writes data to this block, it will write to the dynamic memory first. Will convert the FILE * to FILE descriptors, and then use the write function directly to a FILE, initialized again to buffer the writing position, read data will put data into the buffer, until all the current buffer read, will only be read from the FILE again and again for filebuf buffer size is fixed, it There will be no expansion.

So in the case of Filebuf, the buffer is the requested dynamic memory, and the external device is the file. Filebuf is a standard three-tier structure, both logically and implemented

1.2.3 Low-level implementation of IOstream

For istream,ostream, and iostream, their buffers use Streambuf, but the streambuf constructor is type-protected, so there is no way to generate an object directly, which is understandable because streambuf does not provide buffers, There is no external device provided, so it is not intended to be used directly, just as a base class for stringbuf and Filebuf calls.

If you want to use istream,ostream, and iostream, you need to pass them a usable buffer object, such as a Filebuf object, which is usable, but it’s better to use fstream directly, so for the three basic template classes, since they can’t be used directly, There is no two-tier structure or three-tier structure.

2. Implementation of standard IO global variables CIN and Cout

The iostream class cannot be used directly, but cin is of type istream and cout is of type ostream. In fact, standard IO defines two other ostream types: cerr and CLOG, so they can be used directly.

In the iostream header, we define a global static variable like this:

static ios_base::Init __ioinit;
Copy the code

Ios_base ::Init is a class type defined in the iOS_Base.h header. Its constructor is implemented as follows:

  ios_base::Init::Init()
  {
    if (__gnu_cxx::__exchange_and_add_dispatch(&_S_refcount, 1) = =0)
      {
	// Standard streams default to synced with "C" operations.
	_S_synced_with_stdio = true;

	new (&buf_cout_sync) stdio_sync_filebuf<char>(stdout);
	new (&buf_cin_sync) stdio_sync_filebuf<char>(stdin);
	new (&buf_cerr_sync) stdio_sync_filebuf<char>(stderr);

	// The standard streams are constructed once only and never
	// destroyed.
	new (&cout) ostream(&buf_cout_sync);
	new (&cin) istream(&buf_cin_sync);
	new (&cerr) ostream(&buf_cerr_sync);
	new (&clog) ostream(&buf_cerr_sync);
	cin.tie(&cout);
	cerr.setf(ios_base::unitbuf);
	// _GLIBCXX_RESOLVE_LIB_DEFECTS
	// 455. cerr::tie() and wcerr::tie() are overspecified.
	cerr.tie(&cout);

#ifdef _GLIBCXX_USE_WCHAR_T
	new (&buf_wcout_sync) stdio_sync_filebuf<wchar_t>(stdout);
	new (&buf_wcin_sync) stdio_sync_filebuf<wchar_t>(stdin);
	new (&buf_wcerr_sync) stdio_sync_filebuf<wchar_t>(stderr);

	new (&wcout) wostream(&buf_wcout_sync);
	new (&wcin) wistream(&buf_wcin_sync);
	new (&wcerr) wostream(&buf_wcerr_sync);
	new (&wclog) wostream(&buf_wcerr_sync);
	wcin.tie(&wcout);
	wcerr.setf(ios_base::unitbuf);
	wcerr.tie(&wcout);
#endif

	// NB: Have to set refcount above one, so that standard
	// streams are not re-initialized with uses of ios_base::Init
	// besides <iostream> static object, ie just using <ios> with
	// ios_base::Init objects.
	__gnu_cxx::__atomic_add_dispatch(&_S_refcount, 1); }}Copy the code

In cin, for example, we can see that we are actually passing in an object of type stdio_sync_filebuf during construction, so we know that istream only accepts objects of type Streambuf, Stdio_sync_filebuf is derived from basic_streambuf, so you can guess that stdio_sync_filebuf is derived from streambuf. If you go to the stdio_sync_filebuf header, you can see that stdio_sync_filebuf is derived from basic_streambuf.

For stdio_sync_FILEbuf, there is no buffer, but it is associated with the peripheral keyboard and screen based on the file Pointers stdin, stdout, and stderr passed in, so cin reads directly from the keyboard via stdin. Cout is output directly to the screen via stdout.

Cin, COUT, CERR and CLOG have only two layers of program and external device. However, there is still a bit of confusion. According to the code, they actually open the file and then read and write the file, so how can it be displayed on external device.

The standard input and output are implemented differently depending on the operating system. Here we take Linux as an example to illustrate.

In Linux, there are three standard input and output files, stdin, stdout, and stderr, which are all in the /dev directory. As you can see from the previous chapter, cout actually opens the file /dev/stdout, which is a soft link. /proc/self.f/1 links to /dev/pts-0, which represents the currently open terminal. For example, the current terminal is shown in the following figure:

In this way, the input and output of each program actually receive the input and output of the current terminal, and I will write this point without further elaboration.