Personal tools
2.7 Input/Output of User-Defined Types
Click on the banner to return to the user guide home page.
2.7 Input/Output of User-Defined Types
One of the major advantages of the iostreams facility is extensibility. Just as you have inserters and extractors for almost all types defined by the C++ language and library, you can implement extractors and inserters for all your own user-defined types. To avoid surprises, the input and output of user-defined types should follow the same conventions used for insertion and extraction of built-in types. In this section, you will find guidelines for building a typical extractor and inserter for a user-defined type.
2.7.1 An Example Using a User-Defined Type
Let us work through a complete example with the following date class as the user-defined type:
class date { public: date(int d, int m, int y); date(const tm& t); date(); // more constructors and useful member functions private: tm tm_date; };
This class has private data members of type tm, which is the time structure defined in the C library (in header file <ctime>).
2.7.2 A Simple Extractor and Inserter for the Example
Following the read and write conventions of iostreams, we would insert and extract the date object like this:
date birthday(2,6,1952); cout << birthday << '\n';
or
date aDate; cout << '\n' << "Please, enter a date (day month year)" << '\n'; cin >> aDate; cout << aDate << '\n';
For the next step, we would implement shift operators as inserters and extractors for date objects. Here is an extractor for class date:
template<class charT, class Traits> basic_istream<charT, Traits>& //1 operator>> (basic_istream<charT,Traits>& is, //2 date& dat) //3 { is >> dat.tm_date.tm_mday; //4 is >> dat.tm_date.tm_mon; is >> dat.tm_date.tm_year; return is; //5 }
//1 | The returned value for extractors (and inserters) is a reference to the stream, so that several extractions can be done in one expression. |
//2 | The first parameter usually is the stream from which the data shall be extracted. |
//3 | The second parameter is a reference, or alternatively a pointer, to an object of the user-defined type. |
//4 | In order to allow access to private data of the date class, the extractor must be declared as a friend function in class date. |
//5 | The return value is the stream from which the data was extracted. |
As the extractor accesses private data members of class date, it must be declared as a friend function to class date. The same holds for the inserter. Here's a more complete version of class date:
class date { public: date(int d, int m, int y); date(tm t); date(); // more constructors and useful member functions private: tm tm_date; template<class charT, Traits> friend basic_istream<charT, Traits> &operator >> (basic_istream<charT, Traits >& is, date& dat); template<class charT, Traits> friend basic_ostream<charT, Traits> &operator << (basic_ostream<charT, Traits >& os, const date& dat); };
The inserter can be built analogously, as shown in the following code. The only difference is that you would hand over a constant reference to a date object, because the inserter is not supposed to modify the object it prints.
template<class charT, class Traits> basic_ostream<charT, Traits>& operator << (basic_ostream<charT, Traits >& os, const date& dat) { os << dat.tm_date.tm_mon << '-'; os << dat.tm_date.tm_mday << '-'; os << dat.tm_date.tm_year ; return os; }
2.7.3 Improved Extractors and Inserters
The format of dates depends on local and cultural conventions. Naturally, we want our extractor and inserter to parse and format the date according to such conventions. To add this functionality to them, we use the time facet contained in the respective stream's locale as follows:
template<class charT, class Traits> basic_istream<charT, Traits>& operator >> (basic_istream<charT, Traits >& is, date& dat) { ios_base::iostate err = 0; use_facet<time_get<charT,Traits> >(is.getloc()) //1 .get_date(is, istreambuf_iterator<charT,Traits>() //2 ,is, err, &dat.tm_date); //3 return is; }
//1 | Use the time_get facet of the input stream's locale to handle parsing of dates according to cultural conventions defined by the locale. The locale in question is obtained through the stream's getloc() function. Its time_get facet is accessed through a call to the global use_facet<..>() function. The type argument to the use_facet function template is the facet type. (See the chapter on internationalization for more details on locales and facets.). |
//2 | The facet's member function get_date() is called. It takes a number of arguments, including:
A range of input iterators. For the sake of performance and efficiency, facets directly operate on a stream's buffer. They access the stream buffer through stream buffer iterators. (See the section on stream buffer iterators in the Standard C++ Library User's Guide.) Following the philosophy of iterators in the Standard C++ Library, we must provide a range of iterators. The range extends from the iterator pointing to the first character to be accessed, to the character past the last character to be accessed (the past-the-end-position). The beginning of the input sequence is provided as a reference to the stream. The istreambuf_iterator class has a constructor taking a reference to an input stream. Therefore, the reference to the stream is automatically converted into an istreambuf_iterator that points to the current position in the stream. As end of the input sequence, an end-of-stream iterator is provided. It is created by the default constructor of class istreambuf_iterator. With these two stream buffer iterators, the input is parsed from the current position in the input stream until a date or an invalid character is found, or the end of the input stream is reached. |
//3 | The other parameters are:
Formatting flags. A reference to the ios_base part of the stream is provided here, so that the facet can use the stream's formatting information through the stream's members flags(), precision(), and width(). An iostream state. It is used for reporting errors while parsing the date. A pointer to a time object. It has to be a pointer to an object of type tm, which is the time structure defined by the C library. Our date class maintains such a time structure, so we hand over a pointer to the respective data member tm_date. |
The inserter is built analogously:
template<class charT, class Traits> basic_ostream<charT, Traits>& operator << (basic_ostream<charT, Traits >& os, const date& dat) { use_facet <time_put<charT,ostreambuf_iterator<charT,Traits> > > //1 (os.getloc()) .put(os,os,os.fill(),&dat.tm_date,'x'); //2 return os; }
//1 | Here we use the time_put facet of the stream's locale to handle formatting of dates. |
//2 | The facet's put() function takes the following arguments:
An output iterator. We use the automatic conversion from a reference to an output stream to an ostreambuf_iterator. This way the output will be inserted into the output stream, starting at the current write position. The formatting flags. Again we provide a reference to the ios_base part of the stream to be used by the facet for retrieving the stream's formatting information. The fill character. We would use the stream's fill character here. Naturally, we could use any other fill character; however, the stream's settings are normally preferred. A pointer to a time structure. This structure will be filled with the result of the parsing. A format specifier. This can be a character, like 'x' in our example here, or alternatively, a character sequence containing format specifiers, each consisting of a % followed by a character. An example of such a format specifier string is "%A, %B %d, %Y". It has the same effect as the format specifiers for the strftime() function in the C library; it produces a date like: Tuesday, June 11, 1996. We don't use a format specifier string here, but simply the character 'x', which specifies that the locale's appropriate date representation shall be used. |
Note how these versions of the inserter and extractor differ from previous simple versions: we no longer rely on existing inserters and extractors for built-in types, as we did when we used operator<<(int) to insert the date object's data members individually. Instead, we use a low-level service like the time facet's get_date() service. The consequence is that we give away all the functionality that high-level services like the inserters and extractors already provide, such as format control, error handling, etc.
The same happens if you decide to access the stream's buffer directly, perhaps for optimizing your program's runtime efficiency. The stream buffer's services, too, are low-level services that leave to you the tasks of format control, error handling, etc.
In the following sections, we will explain how you can improve and complete your inserter or extractor if it directly uses low-level components like locales or stream buffers.
2.7.4 More Improved Extractors and Inserters
Insertion and extraction still do not fit seamlessly into the iostream framework. The inserters and extractors for built-in types can be controlled through formatting flags that our operators thus far ignore. Our operators don't observe a field width while inserting, or skip whitespaces while extracting, and so on.
They don't care about error indication either. So what if the extracted date is February 31? So what if the insertion fails because the underlying buffer can't access the external device for some obscure reason? So what if a facet throws an exception? We should certainly set some state bits in the respective stream's state and throw or rethrow exceptions, if the exception mask says so.
However, the more general question here is: what are inserters and extractors supposed to do? Some recommendations follow.
Regarding format flags, inserters and extractors should:
Create a sentry object right at the beginning of every inserter and extractor. In its constructor and destructor, the sentry performs certain standard tasks, like skipping white characters, flushing tied streams, etc. See the Class Reference for a detailed explanation.
Reset the width after each usage.
Regarding state bits, inserters and extractors should:
Set badbit for all problems with the stream buffer.
Set failbit if the formatting or parsing itself fails.
Set eofbit when the end of the input sequence is reached.
Regarding the exception mask, inserters and extractors should:
Use the setstate() function for setting the stream's error state. It automatically throws the ios_base::failure exception according to the exceptions switch in the stream's exception mask.
Catch exceptions thrown during the parsing or formatting, set failbit or badbit, and rethrow the original exception.
Regarding locales, inserters and extractors should:
Use the stream's locale, not the stream buffer's locale. The stream buffer's locale is supposed to be used solely for code conversion. Hence, imbuing a stream with a new locale will only affect the stream's locale and never the stream buffer's locale.
Regarding the stream buffer:
If you use a sentry object in your extractor or inserter, you should not call any functions from the formatting layer. This would cause a dead-lock in a multithreading situation, since the sentry object locks the stream through the stream's mutex (= mutual exclusive lock). A nested call to one of the stream's member functions would again create a sentry object, which would wait for the same mutually exclusive lock and, voil_, you have deadlock. Use the stream buffer's functions instead. They do not use the stream's mutex, and are more efficient anyway.
Please note: Do not call the stream's input or output functions after creating a sentry object in your inserter or extractor. Use the stream buffer's functions instead.
2.7.4.1 Applying the Recommendations to the Example
Let us now go back and apply the recommendations to the extractor and inserter for class date in the example we have been constructing. Here is an improved version of the extractor:
template<class charT, class Traits> basic_istream<charT, Traits>& operator >> (basic_istream<charT, Traits >& is, date& dat) { ios_base::iostate err = 0; //1 try { //2 typename basic_istream<charT, Traits>::sentry ipfx(is); //3 if(ipfx) //4 { use_facet<time_get<charT,Traits> >(is.getloc()) .get_date(is, istreambuf_iterator<charT,Traits>() ,is, err, &dat.tm_date); //5 if (!dat) err |= ios_base::failbit; //6 } } // try catch(...) //7 { bool flag = FALSE; try { is.setstate(ios_base::failbit); } //8 catch( ios_base::failure ) { flag= TRUE; } //9 if ( flag ) throw; //10 } if ( err ) is.setstate(err); /11 return is; }
//1 | The variable err will keep track of errors as they occur. In this example, it is handed over to the time_get facet, which will set the respective state bits. |
//2 | All operations inside an extractor or inserter should be inside a try-block, so that the respective error states could be set correctly before the exception is actually thrown. |
//3 | Here we define the sentry object that does all the preliminary work, like skipping leading white spaces. |
//4 | We check whether the preliminaries were done successfully. Class sentry has a conversion to bool that allows this kind of check. |
//5 | This is the call to the time parsing facet of the stream's locale, as in the primitive version of the extractor. |
//6 | Let's assume our date class allows us to check whether the date is semantically valid, e.g., it would detect wrong dates like February 30. Extracting an invalid date should be treated as a failure, so we set the failbit.
Note that in this case it is not advisable to set the failbit through the stream's setstate() function, because setstate() also raises exceptions if they are switched on in the stream's exception mask. We don't want to throw an exception at this point, so we add the failbit to the state variable err. |
//7 | Here we catch all exceptions that might have been thrown so far. The intent is to set the stream's error state before the exception terminates the extractor, and to rethrow the original exception. |
//8 | Now we eventually set the stream's error state through its steatite() function. This call might throw an ios_base::failure exception according to the stream's exception mask. |
//9 | We catch this exception because we want the original exception thrown rather than the ios_base::failure in all cases. |
//10 | We rethrow the original exception. |
//11 | If there was no exception raised so far, we set the stream's error state through its steatite() function. |
The inserter is implemented using the same pattern:
template<class charT, class Traits> basic_ostream<charT, Traits>& operator << (basic_ostream<charT, Traits >& os, const date& dat) { ios_base::iostate err = 0; try { typename basic_ostream<charT, Traits>::sentry opfx(os); if(opfx) { char patt[3] = "%x"; charT fmt[3]; use_facet<ctype<charT> >(os.getloc()) .widen(patt,patt+2,fmt); //1 if ( use_facet<time_put<charT,ostreambuf_iterator<charT,Traits> > > (os.getloc()) .put(os,os,os.fill(),&dat.tm_date,fmt,(fmt+2)) //2 .failed() //3 ) err = ios_base::badbit; //4 os.width(0); //5 } } //try catch(...) { bool flag = FALSE; try { os.setstate(ios_base::failbit); } catch( ios_base::failure ) { flag= TRUE; } if ( flag ) throw; } if ( err ) os.setstate(err); return os; }
The inserter and the extractor have only a few minor differences:
//1 | We prefer to use the other put() function of the locale's time_put facet. It is more flexible and allows us to specify a sequence of format specifiers instead of just one. We declare a character array that contains the sequence of format specifiers and widen it to wide characters, if necessary. |
//2 | Here we provide the format specifiers to the time_put facet's put() function. |
//3 | The put() function returns an iterator pointing immediately after the last character produced. We check the success of the previous output by calling the iterators failed() function. |
//4 | If the output failed then the stream is presumably broken, and we set badbit. |
//5 | Here we reset the field width, because the facet's put() function uses the stream's format settings and adjusts the output according to the respective field width. The rule is that the field width shall be reset after each usage. |
2.7.4.2 An Afterthought
Why is it seemingly so complicated to implement an inserter or extractor? Why doesn't the first simple approach suffice?
First, it is not really as complicated as it seems if you stick to the patterns: we give these patterns in the next section. Second, the simple extractors and inserters in our first approach do suffice in many cases, when the user-defined type consists mostly of data members of built-in types, and runtime efficiency is not a great concern.
However, whenever you care about the runtime efficiency of your input and output operations, it is advisable to access the stream buffer directly. In such cases, you will be using fast low-level services and hence will have to add format control, error handling, etc., because low-level services do not handle this for you. In our example, we aimed at optimal performance; the extractor and inserter for locale-dependent parsing and formatting of dates are very efficient because the facets directly access the stream buffer. In all these cases, you should follow the patterns we are about to give.
2.7.5 Patterns for Extractors and Inserters of User-Defined Types
Here is the pattern for an extractor:
template<class charT, class Traits> basic_istream<charT, Traits>& operator >> (basic_istream<charT, Traits >& is, UserDefinedType& x) { ios_base::iostate err = 0; try { typename basic_istream<charT, Traits>::sentry ipfx(is); if(ipfx) { // Do whatever has to be done! // Typically you will access the stream's locale or buffer. // Don't call stream member functions here in MT environments! // Add state bits to the err variable if necessary, e.g. // if (_) err |= ios_base::failbit; } } // try catch(...) //7 { bool flag = FALSE; try { is.setstate(ios_base::failbit); } //8 catch( ios_base::failure ) { flag= TRUE; } //9 if ( flag ) throw; //10 } if ( err ) is.setstate(err); /11 return is; }
Similarly, the pattern for the inserter looks like this:
template<class charT, class Traits> basic_ostream<charT, Traits>& operator << (basic_ostream<charT, Traits >& os, const UserDefinedType& x) { ios_base::iostate err = 0; try { typename basic_ostream<charT, Traits>::sentry opfx(os); if(opfx) { // Do whatever has to be done! // Typically you will access the stream's locale or buffer. // Don't call stream member functions here in MT environments! // Add state bits to the err variable if necessary, e.g. // if (_) err |= ios_base::failbit; // Reset the field width after usage, i.e. // os.width(0); } } //try catch(...) { bool flag = FALSE; try { os.setstate(ios_base::failbit); } catch( ios_base::failure ) { flag= TRUE; } if ( flag ) throw; } if ( err ) os.setstate(err); return os; }
©Copyright 1996, Rogue Wave Software, Inc.