We have to take some hurdles, but the result is fantastic.
First we see some basics. Then I'll show the snares and pitfalls that must be circumvented. But then we do the decisive step to the bright side of C++ streaming standard, that nothing compares to it.
This Tutorial deals with the following topics:
Catchwords
- Streaming C++ STL
- Memory Stream
- Binary Stream
- cout & Managed Code, Managed Objects
- StringStream
- Template
- Mixing Native and Managed Types
Motivation
I started this work, because on one hand I had a datastorage in native C++ and on the other a UI in .NET. How could the Objects easy interoperate, and ideally in a general way?
Having weighed the pros and cons, I chose the binary representation in a Memory-Stream.
The objective is the exchange of object-data between applications on the same system. No handling of different codepages or MSB/LSB question.
To exemplary exchange Name, Account, Count either-way. Like (in pseudo-code):
Stream << System::String
<< System::Double
<< System::Int32.
Stream >> std::string
>> std::double
>> std::int.
just quick and safe and duplex.
An early decision I made: Because System::String
always is WideChar, I choose std::wstring
on the other side. To convert between std::string or std::wstring
is not part of my Binary-Stream.
Synonyms
Just a word on the many synonyms that could be confused sometimes.
Native (high quality) C++ Standard <-> Managed (MS) C++ Code; namespace System <-> namespace std; ref class <-> (native) class; String <-> string; CLR <-> STL; .NET ATL MFC <-> STL;
even "safe" <-> "unsafe" code (an idiotic MS promotion jingle).
Mostly I will talk about Ref-Class and Native-Class or CLR vs. STL. I don't use ATL or MFC here.
Table of content
Downloads
You can download Dokuments.zip, with a detailed description of each library and executable plus a tutorial in .html files.
The sources are structured in Finals, Exercises and Concepts.
Binary Stream
#define WIN32_LEAN_AND_MEAN
#include <iostream>
#include <string>
using namespace std;
class Bytes
{
unsigned char *pusc;
bool initialized;
unsigned char *ppos;
public:
void init(int size);
void free();
void startreading();
Bytes();
~Bytes();
Bytes& operator<<(int &val);
Bytes& operator>>(int &val);
Bytes& operator<<(double &val);
Bytes& operator>>(double &val);
Bytes& operator<<(wstring &val);
Bytes& operator>>(wstring &val);
};
void main()
{
cout << "++++++ store data to Bytes-Stream ++++++\n";
Bytes binstream;
int bsize;
int a = 123;
bsize = sizeof(a);
binstream.init(bsize);
binstream << a;
int b;
binstream.startreading();
binstream >> b;
binstream.free();
cout << "int b=" << b << endl;
double d = 456.789;
bsize = sizeof(d);
binstream.init(bsize);
binstream << d;
double e;
binstream.startreading();
binstream >> e;
binstream.free();
cout << "double d=" << d << endl;
wstring ws(L"a String of Wide-Chars");
bsize = sizeof(int) + (sizeof(wchar_t) * (int)(ws.length()));
binstream.init(bsize);
binstream << ws;
wstring xs;
binstream.startreading();
binstream >> xs;
binstream.free();
wcout << "wstring xs=\"" << xs << "\"" << endl;
}
void Bytes::init(int size)
{
pusc = new unsigned char[size];
ppos = pusc;
initialized = true;
}
void Bytes::free()
{
delete [] pusc;
pusc = NULL;
ppos = pusc;
initialized = false;
}
void Bytes::startreading()
{
ppos = pusc;
}
Bytes::Bytes()
:pusc(NULL), initialized(false), ppos(pusc)
{}
Bytes::~Bytes()
{
if (initialized) free();
}
Bytes& Bytes::operator<<(int &val)
{
int *pdest = (int*)ppos;
*pdest++ = val;
ppos = (unsigned char*)pdest;
return *this;
}
Bytes& Bytes::operator>>(int &val)
{
int *psrc = (int*)ppos;
val = *psrc++;
ppos = (unsigned char*)psrc;
return *this;
}
Bytes& Bytes::operator<<(double &val)
{
double *pdest = (double*)ppos;
*pdest++ = val;
ppos = (unsigned char*)pdest;
return *this;
}
Bytes& Bytes::operator>>(double &val)
{
double *psrc = (double*)ppos;
val = *psrc++;
ppos = (unsigned char*)psrc;
return *this;
}
Bytes& Bytes::operator<<(wstring &val)
{
int wsize = (int)val.length();
*this << wsize;
wchar_t *pdest = (wchar_t*)ppos;
const wchar_t *psrc = val.c_str();
while (*psrc != L'\0')
*pdest++ = *psrc++;
ppos = (unsigned char*)pdest;
return *this;
}
Bytes& Bytes::operator>>(wstring &val)
{
int wsize;
*this >> wsize;
val = wstring((wchar_t*)ppos, wsize);
ppos += wsize * sizeof(wchar_t);
return *this;
}
Example: T3_Object1
Preface: Generally we have two kinds of fundamental data: Fixed-Length, Variable-Length.
Fixed-Length I store unpacked with their size, Variable-Length I store with its count of elements at the beginning and the elements afterwards, no delimiters.
If we have it done for C++ Standard-Class we go to the "save" "managed" "MS" Ref-Class.
Oops-a-daisy
Now an obstacle, I anticipated. because System::String
is not implemented bidirectional. There are many ways to build System::String
from string, wstring, C-string, but reverse? Why no System::String.c_str()
conversion; stupid!
Finally I found PtrToStringChars()
in <vcclr.h>
and pin_ptr<>
, interior_ptr<>
in namespace cli
. After processing big-data from the Internet with the eye and brain.
It's implemented in StringConvert.cpp and we use it here now.
We have changes in the syntax by declaring an Object-Reference with '%' instead of '&'.
If a managed object resides on the Garbage-Collected-Heap the type is suffixed with '^' and members are referenced by pointer '->' systax.
Datatypes System::Int32
and System::Double
have a direct compatibility to the corresponding standard int, double.
System::String
can be converted as described above.
Example T4_Bytes2
A great hurdle
Basic System::-Types work with stream, but with a Ref-Class-Object we get the friends-problem.
error C3809: 'RSimple': a managed type cannot have any friend functions/classes/interfaces
"Is this the end _ _ _", taa taa tataa?
The "do nothing" operation
Don't worry, there are still other possibilities to store the data to the stream and keeping the Members private. Let's see.
A popular one is a void method that takes a reference of the stream as parameter and puts the members onto the stream. Quite good, but it's a rupture of the streaming syntax of continued shift operations.
I think about a method, which takes a stream-reference to put the members on it and returns the reference as return-value too.
The method might look like:
stream& Class::Out( stream& )
stream& Class::In( stream& ).
and the call would be like:
stream << Class::Out(stream)
stream >> Class::In(stream).
But be aware, that it comes to a new syntactical situation, which has no common behaviour and defaults sometimes to what you wouldn’t expect.
If the method returns with stream & it leads to the constellation of
stream << stream
which means in global function declaration
stream& operator<<( stream&, stream& )
or as method of our own stream-class
stream& stream::operator<<( stream& )
And for operator>> likewise.
We must say, what the compiler has to do now. The intuitive behaviour is appended the right to the left, or extract from left to right. But in our case we get back the identic stream, we gave to the method before. There is nothing left to append. All happened inside the method. It's important to understand this short demonstration:
#define WIN32_LEAN_AND_MEAN
#include <string>
#include <iostream>
#include <sstream>
using namespace std;
stringstream& operator<<(stringstream &left, stringstream &right);
class Mclass
{
string member;
public:
Mclass():member("Mclass::string"){}
stringstream& Out(stringstream& str)
{
str << member;
return str;
}
};
stringstream& operator<<(stringstream &str, Mclass &obj)
{
str << obj.Out(str);
return str;
}
void main()
{
Mclass mc;
stringstream as, bs;
as << "+++++";
bs << "'''''";
bs << mc;
cout << bs.str() << endl;
bs << as;
cout << bs.str() << endl;
cout << ".. as" << as.str() << endl;
}
stringstream& operator<<(stringstream &left, stringstream &right)
{
if (&left == &right)
{
cout << " + ident +\n";
return left;
}
else
{
cout << " + different +\n";
left << right.str();
}
return left;
}
As we in our stream control all operations, we don't allow shifting between two objects and always return the left object.
"We are unstoppable!" :-D Lets implement that pattern:
stream& MyClass::Out( stream &str )
{
str << this->member ...;
return str;
}
stream& MyClass::In( stream &str )
{
str >> this->member ...;
return str;
}
stream& stream::operator<<( stream &right )
{
return *this;
}
stream& stream::operator>>( stream &right )
{
return *this;
}
stream& operator<<( stream &str, MyClass &obj )
{
str << obj.Out(str);
return str;
}
stream& operator>>( stream &str, MyClass &obj )
{
str >> obj.In(str);
return str;
}
Now the approach of the former example works with "save-" "managed-" "MS-" code too. Yahooo!
The absolute advantage of this pattern is, to shift objects private members in a class-method without having the stream as a friend. It works with any stream :-)
Are we ready now? Rather we mastered the first step to a coming Safari (through oasis and desert), but we can build on that. But now we get the efforts of the level.
What I want to point out!
Once more about the "do nothing" operation. Because managed Ref-Classes reject the "friend" declaration, we have to use an alternate pattern of streaming Class-Members, as shown above.
I want to lead you to the point now. With that little example T4_Bytes2 we are in the good situation, to get a clear view of the conditions.
We have implemented the complete interface of a stream by ourself. And(!!) we introduced full streaming-behaviour to Ref-Classes by omitting the common "friend" syntax and implementing an alternative. And here we are free of some sideeffects of a precarious implementation of STL-streams (at least in VC2005), I will show you later!
Now we can go to the declaration of our own stream in Bytes.h. Here are the two lines of declaring the "do nothing".
If you comment them out
and compile the file t4_bytes2.cpp now, the compiler shows you the two statements
str << obj->Out(str);
str >> obj->In(str);
where this functionality is requested.
More about this find her in the chapter "Effects or Defects?"
A comfotable interface for classes
On the previous page, we explored the technical principals of storing and retrieving a binary datagram of an object's data to and from a Memory-Stream. And how it can be implemented for Common-C++ classes, and MS-Managed-C++ Ref-Classes as well.
Here now we look at the refinement to a usable utility.
The intention is, to get a package of an object and to put it, like on the network, on the inner-application control flow. The stream should be a message of an object, not a storage, as streams usually are seen.
Like on the network, a message that is sent, but not consumed, is worthless. This perspective will impact on some implementation details later.
Usability
Now we must put some comfort to the usability. As you imagine, the chosen solution works very strict and some mistakes could happen. First of all, we deal with rough memory and have to check the boundaries. Just as important is the usability. E.g. wrong size calculated, the types calculated, written or read differ, you see. Especially with binary data, the user needs quick and strong notice of what could have failed.
Therefore: First the size of the binary representation is calculated and fixed. The stored data must exactly meet this size. To store more is forbidden and less is an error too.
It's impossible to retrieve data from a stream, that's not proper built.
Not consuming all the stored data is an error.
And inhibit typos like this:
Stream >> Name
<< Account
>> Count;
Syntactically correct but rare intended. I decide it to be an error.
Requests
As previously shown, there are some shortcomings so far, which raise the following requests.
The read/write access is controlled with two different Constructors and during the read/write operations.
If the data is received in a wrong sequence, bricked results are produced and must be controlled optically in the source.
To keep the footprint of the Binary-Stream as lean as possible the implementation gets two parts:
One that holds the data to be dragged around, and another is a helper, that does the work of write, read and control. And there is an Interface to the user defined (UD-)classes that can be inherited. It defines the at least required methods of the class and provides services to it.
Due to the syntactical differences of native- and managed code, there are two helpers and two interfaces.
The "Native-" components are kept pure native and compile in such projects without /clr-switch.
Designing the interface
Now we look at the interfaces here:
The principals i discuss for Standatd-C++. /CLR is a little bit different, we will look at it later.
What are our elements?
- we have a message-envelope to retrieve the data, to drag it around and to deliver it.
class Bytes;
- the helper that fills and empties the envelope.
class BinaryN;
- an interface to the UD-class.
class INbinary;
- the user's class, inheriting the interface.
class NEntity : public INbinary;
Supplied syntax
The statements, I want to supply (in pseudo-code):
- produce the data-message
Bytes << NEntity;
- deliver the data-message
Bytes >> NEntity;
- produce the data-message by function
Bytes foo();
- dragging around the data-message
Bytes = foo();
- deliver the data-message by function
void foo( Bytes& );
Class layout
This leads to the following layout of the classes:
The binary stream Bytes
class Bytes
{
friend class BinaryN;
friend class INbinary;
unsigned char *pusc;
unsigned char *ppos;
bool initialized;
bool toDelete;
int size;
void init(int sz);
void free();
void nulling();
void movesecure(Bytes &dest, Bytes &src);
void error(string e);
public:
Bytes& operator=(Bytes&);
Bytes& operator<<(INbinary&);
Bytes& operator>>(INbinary&);
Bytes();
Bytes(const Bytes&);
~Bytes();
};
The helper of Bytes for storing and retrieving
class BinaryN
{
Bytes *theMem;
unsigned char *mem;
unsigned char *pend;
enum Operation{o_put, o_get}op;
BinaryN();
void allowed(Operation o);
void error(string e);
public:
BinaryN(int sz, Bytes &abuf);
BinaryN(Bytes &abuf);
~BinaryN();
void deleteMem();
BinaryN& operator<<(unsigned __int32 &val);
BinaryN& operator>>(unsigned __int32 &val);
BinaryN& operator<<(int &val);
BinaryN& operator>>(int &val);
BinaryN& operator<<(wstring &val);
BinaryN& operator>>(wstring &val);
BinaryN& operator<<(double &val);
BinaryN& operator>>(double &val);
BinaryN& operator<<(BinaryN &right);
BinaryN& operator>>(BinaryN &right);
static __int32 sizeBytes(const wstring &val);
static __int32 sizeBytes(const unsigned __int32 &val);
static __int32 sizeBytes(const int &val);
static __int32 sizeBytes(const double &val);
};
Interface-class, a user-defined (UD-)class (e.g. NEntity) should inherit for easy interaction with Bytes-stream.
class INbinary
{
friend class Bytes;
void storeBytes(Bytes&);
void retrieveBytes(Bytes&);
protected:
string className;
public:
virtual BinaryN& Out(BinaryN &str) = 0;
virtual BinaryN& In(BinaryN &str) = 0;
virtual const __int32 mySize() const = 0;
virtual wostream& Out(wostream &str) = 0;
};
A UD-class inheriting the interface.
class NEntity : public INbinary
{
wstring ss;
double sd;
public:
NEntity(wstring s = L"", double d = 0);
void Text(wstring val);
const __int32 mySize() const;
BinaryN& Out(BinaryN &str);
BinaryN& In(BinaryN &str);
wostream& Out(wostream &str);
};
wostream& operator<<(wostream &str, NEntity &obj);
Special lines of code
In the Bytes-sources you find some special lines of code, I want to explain here, if they are new to you:
#define DEBUG_Bytes_Class
#undef DEBUG_Bytes_Class
#ifdef DEBUG_Bytes_Class
static int count;
public:
int name;
private:
#endif
#ifdef DEBUG_Bytes_Class
name = count++;
cerr << "...\t empty\t_" << name << endl;
#endif
At the start of Bytes' header-file there is a #define, defining a macro-name, immediately followed by an #undef of the same macro-name which invalidates the previous #define. Does that make sense?
It is intended as a switch, helping to debug the sequence of important methods.
If the #undef line is commented out //#undef ... the previous definition stays active. It remains active until the end of the actual compilation, or until another #undef of that macro-name occurs. If the compiler meets an now #ifdef with that macro-name, it is true and additional lines of code are added to the class' declaration and to the definition.
In my case, it's a static counter and an int called 'name'. The counter is incremented by each construction of a new object and gives each instance a unique name. When an instance now executes methods, it always tells its name and the method it's executing. So i had on the Console-output a protocol: who is doing what, when. I like this method to analyse complex situations.
Details of implementation
To create a writer, Bytes-object must be empty.
To create a reader, Bytes-object must hold data exact of the calculated size.
The assignment- and the copy-operation hands over the data-buffer to the left side and the right-side-pointer is NULLed. Therefore the const right-hand object of Copy-constructor must be modified. To do this, the constness of the source-object is casted away by calling the method movesecure(...):
movesecure(*this, const_cast<Bytes&>(src) );
Differences of CLI implementation
CLI introduces its new type-syntax.
A declared managed class ref class CAny; can be defined in two ways; CAny^ A and CAny B.
A reference to a managed class is declared with '%' instead of '&'.
This results in differences in the declarations, but the implementations are very similar.
Likewise to the standard-class interface, we have a managed-class interface
ref class IRbinary;
and a UD-managed-class which inherits that interface
ref class REntity : public IRbinary;
But in class Bytes there is no interface to Ref-Class IRbinary
, although it could be declared like this:
friend ref class IRbinary;
Bytes& operator<<(IRbinary^%);
Bytes& operator>>(IRbinary^%);
Bytes& operator<<(IRbinary%);
Bytes& operator>>(IRbinary%);
The reason is the request of full compatibility to C++ standard (native) code, to compile without CLR, which would be claimed here.
But the shift operators "<<, >>" can be defined globally as well. So i declared the namespace Bytes_Ref
and put them there.
Example BytesInterface shows that implementation.
The plan is realized now. We have a stream, that takes and delivers a binary datagram of an object, that works for standard class and managed class as well.
Are we ready now? Maybe, but we are appetized for more !;-)
Due to the construction, the Binary-Stream is limited to one object. May be an application likes to ship more than one object at once.
Streaming all around
On the previous pages we have done the basics so far. Now we start with streaming all around.
The backbone Memory-Stream will be STL-StringStream and we will use Filestream for persistence.
Supplied syntax
The goal is, to put on or get from a stream a sequence of objects in binary representation.
Syntactically (in pseudo-code):
stream << CAny << ...
stream >> CAny >> ...
Implementation
Because we've done much work previously, we can bring in the harvest now :-)
For user-defined (UD) classes like CAny we have a uniform interface to our binary representation and add this task there.
As shown in chapter 1, we need three elements for each to do it.
stream& operator<<( stream &left, stream &right );
stream& CAny::Out( stream& );
stream& operator<<( stream &str, CAny &obj );
stream& operator>>( stream &left, stream &right );
stream& CAny::In( stream& );
stream& operator>>( stream &str, CAny &obj );
The first belongs globally to the App, the other both to the UD-class CAnny
that inherits from interface-class INbinary
.
It's implemented in StreamDoNothing
, INbinary
, IRbinary
.
The syntax to put on filestream is fs << str.str();
, to get from str << fs.rdbuf();.
Example ByteStream.
Conclusion
Now we have come really very far. Let's see what we've got.
We stream to / from a memory-stream (STL-stringstream here):
str << ob1 << ob2;
str >> ob1 >> ob2;
We stream the members of a complex class to / from the memory-stream (Bytes).
str << name << account << Simple::Out(str) << date.Out(str);
str >> name >> account >> Simple::In(str) >> date.In(str);
We write to file fs << str.str();
.
We read from file str << fs.rdbuf();
.
Streaming all around. Isn’t that fine !:-D
From a basic type to a complex object, to a sequence of objects, to a Memory-Stream, to persistence, to any STL-stream, for Standard-C++ and MS-Managed-C++, and duplex in both directions!
Why am I still a little dissatisfied? First of all I struggled with the STL-stream implementation, and additionally the usability needs some refactoring.
Alternate pattern
Streaming of managed objects with STL-stream.
Intention
Here I want to show the principals of streaming managed objects by STL-Stream in detail, because it's not without pittfalls.
Ostream for managed types
The standard pattern of streaming objects, by having the stream as a friend of the class, does not work for managed Ref-Class. This leeds to the necessity of an alternative, which is used allready in former examples and is explained in detail here.
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#include <iostream>
using namespace std;
using namespace System;
wostream& operator<<(wostream &left, wostream &right);
class Native
{
double member;
public:
Native(double d = 0): member(d)
{}
friend wostream& operator<<(wostream &str, Native &obj);
};
wostream& operator<<(wostream &str, Native &obj)
{
str << "Native::member=" << obj.member;
return str;
}
ref class Managed
{
Double member;
public:
Managed(Double d): member(d)
{}
wostream& Out(wostream &str)
{
str << "Managed::member=" << member;
return str;
}
};
wostream& operator<<(wostream &str, Managed %obj)
{
str << obj.Out(str);
return str;
}
wostream& operator<<(wostream &str, Managed^ %obj)
{
str << obj->Out(str);
return str;
}
wostream& operator<<(wostream &left, wostream &right) {
return left;
}
void main()
{
Native n(123.456);
wcout << n << endl;
Managed^ m = gcnew Managed(789.123);
wcout
<< m << endl
<< *m << endl;
}
It is rich commented and shows the difference of common and alternate pattern with an exemplary implementation.
Alternate pattern with nested classes
This example is to show all the elements of alternate pattern in declaration, definition and usage clear and separated for nested classes.
Example Ostream
Alternate pattern for output and input
Here the alternate pattern is implemented for nested classes for output and input with std::stringstream.
Example Stringstream
For comparison purposes is here an implementation with the standard pattern.
Example TraditionalStream
Syntax pitfall
Now we see one of the traps of STL-stream implementation, one could step in.
And if you look at the example, you see that it's going wrong without any hint by the compiler. It just implements a default behaviour, that's not very useful here.
That's the reason too, why the previous two examples explained the proper syntax that verbose, because you must control it optically, manually in your source, without any help by compiler or linker.
#define WIN32_LEAN_AND_MEAN
#include <string>
#include <iostream>
#include <sstream>
using namespace std;
stringstream& operator<<(stringstream &left, stringstream &right);
class Mclass
{
string member;
public:
Mclass():member("Mclass::string"){}
stringstream& Out(stringstream& str)
{
str << member;
return str;
}
};
stringstream& operator<<(stringstream &str, Mclass &obj)
{
str << obj.Out(str);
return str;
}
void main()
{
Mclass mc;
stringstream as, bs;
as << "+++++";
bs << "'''''";
bs << mc;
cout << bs.str() << endl;
bs << as;
cout << bs.str() << endl;
cout << ".. as" << as.str() << endl;
}
stringstream& operator<<(stringstream &left, stringstream &right)
{
if (&left == &right)
{
cout << " + ident +\n";
return left;
}
else
{
cout << " + different +\n";
left << right.str();
}
return left;
}
In the following, we see more of them
Effects or defects of STL-Stream implementation
As we have already seen in previous Chapters, the alternate pattern of cooperation of stream and object consists of three elements (in pseudo-code):
A) The "do-nothing" (as I call it)
declared as a method of stream-class
stream& stream::operator<<( stream& );
stream& stream::operator>>( stream& );
or declared globally
stream& operator<<( stream&, stream& );
stream& operator>>( stream&, stream& );
B) The class-methods
that access the class-members
stream& AnyClass::Out( stream& );
stream& AnyClass::In( stream& );
C) The stream-to-class operator
that leads the stream to the class-method
declared globally
stream& operator<<( stream&, AnyClass& );
stream& operator>>( stream&, AnyClass& );
or declared as stream-class method
stream& stream::operator<<( AnyClass& );
stream& stream::operator>>( AnyClass& );
The global declaration works for every class, and has no impact to stream's interface at all.
A short, but educative tour you find in
#define WIN32_LEAN_AND_MEAN
#include <string>
#include <iostream>
#include <sstream>
using namespace std;
stringstream& operator<<(stringstream &left, stringstream &right);
class Mclass
{
string member;
public:
Mclass():member("Mclass::string"){}
stringstream& Out(stringstream& str)
{
str << member;
return str;
}
};
stringstream& operator<<(stringstream &str, Mclass &obj)
{
str << obj.Out(str);
return str;
}
void main()
{
Mclass mc;
stringstream as, bs;
as << "+++++";
bs << "'''''";
bs << mc;
cout << bs.str() << endl;
bs << as;
cout << bs.str() << endl;
cout << ".. as" << as.str() << endl;
}
stringstream& operator<<(stringstream &left, stringstream &right)
{
if (&left == &right)
{
cout << " + ident +\n";
return left;
}
else
{
cout << " + different +\n";
left << right.str();
}
return left;
}
More examples in the previous chapters.
A closer look at the streaming syntax
Besides the "do-nothing" another centrepiece of the alternate pattern is using methods in a stream-expression.
So let's examine it in detail in
Example LeftToRight
The streaming operators '<<' and '>>' have left-to-right associativity. They are said, but let's see the truth.
Some details of the construction of the example.
It has the following elements:
- a simple class (class Txt)
- three streams
std::ostream
class CMyQstr
std::stringstream
- three signatures of function/method
stream& Txt::Out( stream& ) /...::In(...)
stream& strout( stream&, Txt& ) /...strin(...)
string txtXout( Txt& ) /...txtin(...)
- a global
std::string
Gst it collects the out-streamed elements in the sequence they are evaluated - three implementations, one per stream
Ostream.cpp
QueueStream.cpp
StringStream.cpp
If you look at main()
function, you see, that only tree functions are active, the others are all commented out.
It's because this example should be done step by step, following this guided tour.
Further on we will do some modifications of the source, to get a clear view on the effects.
If you execute the first 3 functions
oput1(..) oput2(..) oput3(..)
you get the following output:
********** Studies of '<<', '>>' syntax **********
exploring the left to right associativity
********** Ostream **********
----- oput1 -----
stored: 1a2b3c
read: 1a2b3c
----- oput2 -----
stored: c3b2a1
read: c3b2a1
----- oput3 -----
stored: cba123
read: c3b2a1
stored: means, how the elements are put on stream, and
read: shows, how the expressions have been evaluated.
Although the elements three times are put on stream in the same order, they have been treated different each time.
oput1()
put objects on stream and the elements have been evaluated and stored in left-to-right order.
oput2()
put the objects on stream per function, like by the alternate pattern, and the objects have been evaluated and stored in right-to-left order
oput3()
put the objects on stream by two different functions. The expressions have been evaluated in right-to-left order, but on stream there are two different orders. The values of the function with return-value stream& it took first and in the evaluated right-to-left order. The values of the function with return-value string it took second and in left-to-right order, as they are written in the statement.
Now lets do some modifications of the source, to see some effects of that behaviour.
A) We decorate the output of oput1()
with some literal constants. The values should be outputted like ,1',a' and so on.
The function ostream& operator<<(ostream &str, Txt &obj)
in source Txt.cpp has a commented line. Remove the comment of that line and put the comment to the one above.
On execution you get the output
----- oput1 -----
stored:
1,'a,'2,'b,'3,'c,'
read: 1a2b3c
It's not what we wanted, but understandable, since we know, that it stores the return values of type stream& first, and the others afterwards.
One way to solve it, is to split the streaming-statement into
str << ",";
str << obj.Out(str) << "'";
B) Next we look at the first output and input stream CMyQstr
.
In main()
we uncomment the 3 blocks of MyQueueSream
.
Past execution you see a new output-line got:.
The lines read: and stored: show, that on writing it behaves like ostream, and line got: shows, that on read it restores the values in the correct order, despite of the written sequence.
C) Next we explore std::stringstream.
At first we uncomment the 1. block of STL-StringStream.
On execution the output shows, that it evaluated the expressions like the other both, and line stored: shows the values, each followed by the separator blank.
D) Next we modify the source of sput1(..). As first element of output, we add the output of "-" and write << "-" just before << z1.
Past execution we look at the output and wonder what had happened.
It evaluated like before, but on stream it stored the ostream implementation instead of the stringstream one.
The fact is: Putting the character "-" (or any other basic type) on stringstream corrupted the stringstream to iostream and then the compiler implemented ostream instead of stringstream function. Basic types are corrupting stringstream to iostream!
The workaround is to end the corrupted statement and start a new one like
str
<< "-";
str << z1 ...
What would you say: Is it a bug or a feature (of VS2005, at least)?
But that's not the end of the "features" ;-)
E) We uncomment the next block.
Past execution we see, it behaves likewise CMyQstr.
F) From the 3. block, we uncomment the first two lines, the heading and sput3(..).
As result we see an extra sort of the expressions of the output statement.
It inspected the expressions of the statement and sorted them by return-value before executing them in right-to-left order. The resulting it sorted like CMyQstr
or ostream.
G) We uncomment the rest of the last block now. And we go to StringStream.cpp to uncomment the implementation of sget3(..) too.
Now we get a compilation error C2679:. What's the matter?!
str
>> txtin(z1)
>> strin(str, ta)
>> txtin(z2)
...
The error-message refers to the second expression >>strin(..) and means, it can't continue the statement past txtin(..).
The fact is:
Function txtin(..)
returns a std::string
which corrupts stringstream to iostream. Therefore the compiler is looking for a function like
stringstream& operator>>( istream&, stringstream& );
You can try it, if you declare that signature at the beginning of the source file. You'll see, the compiler is satisfied. But it's impossible to implement that operation well.
But you can implement a do-nothing of std::istream
instead.
istream& operator>>( istream&, istream& );
Do it in StreamDoNothing.h and you'll see, it works.
But it's not a good solution, to feed the compiler with wrong stream implementations. The better workaround is, to remove the bad istream declaration again and to put the corrupting expressions into a separate statement and continue with a new statement afterwards. Don’t forget: You must do the output likewise!
str
>> txtin(z1)
>> txtin(z2)
>> txtin(z3)
;
str
>> strin(str, ta)
>> strin(str, tb)
>> strin(str, tc)
;
But there is still another major problem with STL-stringstream: It has no clear()
or truncate()
method, at least in VC2005. So it grows to infinite, if you use it repeatedly. Deadly for long-running applications. There are two workarounds, if you are dealing properly with internals of its implementation. There is a particularized discussion about in example ByteStream
and ByteStreamT
.
Now we have seen, how streams work with functions and methods.
And we have found workarounds of all that hurdles, brought about this hellish implementation of STL-streams. Preventable problems that let some despair.
More Examples
Some examples about using methods in streaming-expressions.
OstrFormatter, Manipulator
There is a better way
But the more beautiful part is coming now.
We turn the page on the problematic STL-streams and see how comfortable it is, to develop a stream on one’s own.
- no stream-corruption
- no delimiters
- no problem with whitespace in strings
- no growth to infinite, no dealing with internals
Streams by ones own
The first stream by our own we did in example LeftToRight, in class CMyQstr(.h .cpp).
The best choice at all for a stream-like behaviour (first in - first out) is datastructure Queue, because it grows on write (push) and shrinks on read (pop) automatically.
Class CMyQstr
is kept simple, just streaming std::string
type, but its declaration shows all elements, a stream needs.
class CMyQstr
{
vector<string> qstring;
vector<string>::iterator iter;
public:
CMyQstr();
CMyQstr& operator<<(string &v);
CMyQstr& operator>>(string &v);
CMyQstr& operator<<(CMyQstr &right); CMyQstr& operator>>(CMyQstr &right); };
And the declaration of a streamable class:
class MyUDC
{
string st;
public:
MyUDC(char v[]);
MyUDC(string &v = string(". "));
CMyQstr& Out(CMyQstr &str);
CMyQstr& In(CMyQstr &str);
ostream& Out(ostream &str);
};
ostream& operator<<(ostream &str, MyUDC &obj);
CMyQstr& operator<<(CMyQstr &str, MyUDC &obj);
CMyQstr& operator>>(CMyQstr &str, MyUDC &obj);
A stream for nested Objects
Example MyStreamX shows a stream for the three basic datatypes string, integral, float and how it works from basic type to nested classes.
Here you can see how pretty and handsome streaming is made by C++, if it's not destroyed by a hellish implementation:
class COrder
{
string header;
CArticle article;
CPerson person;
public:
CMyStream& Out(CMyStream &str);
CMyStream& In(CMyStream &str);
};
CMyStream& operator<<(CMyStream &str, COrder &obj);
CMyStream& operator>>(CMyStream &str, COrder &obj);
CMyStream& COrder::Out(CMyStream &str)
{
str
<< header
<< article
<< person
;
return str;
}
CMyStream& COrder::In(CMyStream &str)
{
str
>> header
>> article
>> person
;
return str;
}
No stream corruption, no delimiter struggling.
Now we have seen the convenience of a tidy stream and make ours ready to use with likely templates.
Wrapper instead of inheritance
Streaming all around. That's fine, I think.
But to inherit the interface has a significant impact on class' design that dissatisfies me. Each class must inherit, to become binary streamable and if you derive from them, you easy get it twice in the new class. Additionally the importance of the streaming interface is just for a short time, right to the end or right to the beginning of an object's lifetime.
To circumvent these complications, a wrapper seems the better way. And it looks really smart ;)
The wrapper-class takes a reference to class CAnyN
to binSerial
and redirects each call of INbinary
's interface to CAnyN
's implementation.
Implementation for Native-Class
typedef CAnyN TYPE;
class BinStreamN: public INbinary
{
TYPE &binSerial;
BinStreamN(); public:
BinStreamN(TYPE &objRef)
:binSerial(objRef)
{
className = "BytesWrapper";
}
BinaryN& Out(BinaryN &str)
{
return binSerial.Out(str);
}
BinaryN& In(BinaryN &str)
{
return binSerial.In(str);
}
const __int32 mySize() const
{
return binSerial.mySize();
}
wostream& Out(wostream &str)
{
str << "Cname=\"" << StringConvert::wstr(className) << "\" ";
return binSerial.Out(str);
}
};
wostream& operator<<(wostream &str, BinStreamN &obj)
{
return obj.Out(str);
}
Declaration for Ref-Class
ref class CAnyR;
ref class BinStreamR: public IRbinary
{
CAnyR ^binSerial;
BinStreamR(); public:
BinStreamR(CAnyR^ %objRef);
virtual BinaryR& Out(BinaryR &str) override;
virtual BinaryR& In(BinaryR &str) override;
virtual const __int32 mySize() override;
virtual wostream& Out(wostream &str) override;
};
wostream& operator<<(wostream &str, BinStreamR^ %obj);
wostream& operator<<(wostream &str, BinStreamR %obj);
As you see, the implementation is the same for any class. Therefore it's a proper candidate to become a template. We will do it later on.
Let's go to see how to do with templates.
Template
When I started with templates, I found many in-depth articles, but I was still struggling with the syntax-basics.
So I'll not give a talk about the syntax, but show concrete implementations of the basics.
First of all the naming Function-Template or Template-Function.
Like class and object, where object is an instance of a class, Template-Function is a by compiler implemented Function-Template. Class-Template and Template-Class likewise.
At the beginning a function-template. example TemplateFoo.
Deriving a template, I always start with typedef
, because I look at a template as a typedef for multiple types. And as an advantage, I don’t have to deal with template syntax at the beginning, i am still inexpertly in detail.
So I write:
typedef int TYPE;
void show(const TYPE &val)
{
cout << "TYPE: " << val << endl;
}
and I test it with different types like
int i(123);
show(i);
If it works, I substitute the typedef
with template
template<typename TYPE>
void show(const TYPE &val)
{
cout << "TYPE: " << val << endl;
}
and the template is done :-)
Note: typename
is an alias
of class
in this context.
As next a specialisation of that template, because a string must be treated different form fixed-length types more often than not.
You declare it to be a template and write an implementation with the concrete type:
template<>
void show(const string &val)
{
cout << "string: " << val << endl;
}
Also important is the information, that for templates the declaration and the definition must be in a header-file, because the definition must be seen by compiletime. Otherwise the linker will report an error.
As next template overloading:
void show(const char val[])
{
cout << "char[]: " << val << endl;
}
is not another specialisation of function-template show(..)
, because the variable has changed from being a reference to an array.
Here you put the declaration
void show(const char val[]);
to the header-file and the implementation to the .cpp file.
On call of a function-template some or all types can be defined explicitly too.
For example:
template<class T1, class T2>
void foo(T2 value)
{
T1 conversionType;
...
};
will be called for std::double
and System::Double
Double amount;
foo<double>( amount );
Bingo! That’s quite all, i had to know about templates, to do all the rest you’ll see.
With class-templates the same rules apply.
A class can have method-templates too.
And the concrete writing of a class-template's declaration separate from the class-template's definition, you'll see in the following.
Class-method-template
Example MyStream shows a Queue as StringStream-like implementation.
The stream is generalized to all built-in types by method-templates and by template overloading.
It shows the streaming syntax for nested and inherited classes.
Note: The reference to a parent ref class CManaged;
would look as
(CManaged%)*this
Class-template
Example QueueTemplate implements a typesafe Queue-stream for all standard types and user-defined (UD-)classes.
I developed the UD-classes and the queue-stream-template parallel and had uncertainties here and there.
So I started developing the template with typedef
.
The declaration in file CObjStreamT.h
typedef COrder TYPE;
class CObjStream
{
queue<TYPE> *bq;
public:
CObjStream& operator<<(TYPE&);
CObjStream& operator>>(TYPE&);
CObjStream();
};
and the definition in CObjStreamT.cpp
CObjStream& CObjStream::operator<<(TYPE &obj)
{ ... }
CObjStream& CObjStream::operator>>(TYPE &obj)
{ ... }
CObjStream::CObjStream()
: bq(new queue<TYPE>), op(o_put)
{}
When I switched to template, I had to change the syntax.
All occurrences of the class-name
as a type-definition and as scope-definition had to be changed to class-name<TYPE>
.
And the class declaration and each method implementation had to be prefixed with template<class TYPE>
, that it looked like
template<class TYPE>
class CObjStream
{
queue<TYPE> *bq;
public:
CObjStream<TYPE>& operator<<(TYPE&);
CObjStream<TYPE>& operator>>(TYPE&);
CObjStream();
};
template<class TYPE>
CObjStream<TYPE>& CObjStream<TYPE>::operator<<(TYPE &obj)
{ ... }
template<class TYPE>
CObjStream<TYPE>& CObjStream<TYPE>::operator>>(TYPE &obj)
{ ... }
template<class TYPE>
CObjStream<TYPE>::CObjStream()
: bq(new queue<TYPE>), op(o_put)
{}
And the implementation had to be moved form .cpp-file to header-file.
The stream's interface has grown and very quick i took the help of some macro-definition.
You see it at the beginning of the header-file:
#define CObjStream_TEMPLATE_DEBUG
#undef CObjStream_TEMPLATE_DEBUG //to DEBUG comment this line out
#if defined CObjStream_TEMPLATE_DEBUG
typedef COrder TYPE;
#define CObjStream_TEMPLATE CObjStream
#define TEMPLATE_class
#else
#define CObjStream_TEMPLATE CObjStream<TYPE>
#define TEMPLATE_class template<class TYPE>
#endif //DEBUG
and just to the end of header-file
#if !defined CObjStream_TEMPLATE_DEBUG
#include "CObjStreamT.cpp"
#endif //DEBUG
With that definitions it switches in DEBUG-ON to version typedef and in TEMPLATE-ON to version template.
That is very helpful, because without templates many compilers bring better error-messages.
Try it by yourself :-)
With templates we've got the last chapter to put that all together.
Now we'll make the BinaryStream smooth and handsome.
Interface, Wrapper, Template
Four major tasks are left to be done, to get a final.
- we have to enhance the interface of UD-classes to our stream
- the stream must get an interface to accept all built-in types
- the safety against misusage must be improved. The user needs strong indications, if he is acting with wrong types or in the wrong sequence.
- we need more than one datagram. We strive for a container that takes any number of any objects.
In this chapter here we will do the interface.
In chapter 2 (example BytesInterface) we built the interfaces (two; one for standard and second for managed class) and it does all we need. What could be wrong with it?
Well, the discussion is, that it has a strong implication to class-layout. Especially for managed class, that knows just single inheritance. If it is derived from another class, it can't inherit from the interface-class too. In short, we are looking for a wrapper that's loose coupled to the UD-class. And it should be a template, that fits to any UD-class.
A first discussion of that subject we did in chapter "Wrapper instead of inheritance".
Here we see that class modified as wrapper-class-template for standard-C++ UD-objects.
template<class CANYN>
class BinStreamN: public INbinary
{
CANYN &objReference;
BinStreamN(); public:
BinStreamN(CANYN &objRef)
:objReference(objRef)
{
className = "BinStreamN";
}
BinaryN& Out(BinaryN &str)
{
return objReference.Out(str);
}
BinaryN& In(BinaryN &str)
{
return objReference.In(str);
}
const __int32 mySize() const
{
return objReference.mySize();
}
wostream& Out(wostream &str)
{
str << "Cname=\"" << StringConvert::wstr(className) << "\" ";
return objReference.Out(str);
}
};
template<class CANYN>
wostream& operator<<(wostream &str, BinStreamN<CANYN> &obj){ return obj.Out(str); }
The wrapper inherits the Ibinary interface-class, takes an object-reference and redirects all interface-calls to the referenced object's implementation.
The syntax to instantiate a concrete wrapper and giving it an object-reference is:
for standard class on local stack:
CNative objnative;
BinStreamN<CNative> wrnative( objnative );
for standard class on heap:
CNative *objnative = new CNative();
BinStreamN<CNative> wrnative( *objnative );
for managed ref-class on GC-heap
CRefclass^ objrefclass = gcnew CRefclass();
BinStreamR<CRefclass> wrrefclass( objrefclass );
for managed ref-class on local stack (alike)
CRefclass objrefclass;
BinStreamR<CRefclass> wrrefclass( %objrefclass );
Example _Common implements that wrapper-class-template.
With this user-interface (UI) of UD-class to our Binary-Stream I’m satisfied. The UI-class is loose coupled to the UD-class, design and lifetime of UD-class is independent of UI.
You act with the UD-class as usual:
CNative *objnative = new CNative (); BinStreamN<CNative> wrnative(*objnative ); bstream >> wrnative; objnative->Text(L"!:-O ;-)) :) :D XD :p"); bstream << wrnative;
We are ready to finalize now!
Finals
The remaining three tasks we'll do here.
- we need more than one datagram. We strive for a container that takes any number of any objects.
- the stream must get an interface to accept all built-in types
- the safety against misusage must be improved. The user needs strong indications, if he is acting with wrong types or in the wrong sequence.
Final of ByteStream
Example BytesInterface
It's similar to example ByteStream expanded by the Queue-Stream and improvements.
To accomplish task A), I add a Queue-Stream to class Bytes by deriving.
class BytesQueue : public Bytes
{
queue<Bytes> *bq;
...
Class Bytes acts with the UD-object and datagram and BytesQueue manages these datagrams.
BytesQueue additional provides an interface to STL-streams to deal with any of them. STL‑StringStream and STL‑FileStream are exemplary implemented.
void push();
void pop();
stringstream& Out(stringstream &str);
stringstream& In(stringstream &str);
};
stringstream& operator<<(stringstream &str, BytesQueue &obj);
stringstream& operator>>(stringstream &str, BytesQueue &obj);
To accomplish task B), the helpers to class Bytes
, BinaryN
and BinaryR
get method-templates to operator<<
and operator>>
and template-overloads to the three string-types std::string
, std::wstring
and System::String
.
The method sizeBytes(..)
must become a template too with template-overloads to the string-types.
To accomplish task C), each storage of a basic type is preceded by a typeidentity, that marks the start of the type's datagram and the type of the data that follows. The typeidentity is not unique to all different types. For example an __int64
can be stored and read by a double
, because both have a type-size of 8 bytes. But it's impossible to intermix types of different size or string
with wstring
.
Keep in mind: If you are mixing standard-C++ and managed-C++, there is no corresponding type to std::string
in managed code. System::String
corresponds to std::wstring
only.
And the selfmade ByteStreamT
is ready to use now.
The outstanding advantage of ByteStreamT
With its functional principles it can deal with objects holding any kind of binary data, for example images, audio streams and so on.
Final ByteStreamT consists of the following files:
BinaryN (.h .cpp)
BinaryR (.h .cpp)
BinStreamN (.h)
BinStreamR (.h)
ByteQueueStream (.h .cpp)
Bytes (.h .cpp)
BytesR (.h .cpp)
Hexdump (.h .cpp)
INbinary (.h .cpp)
IRbinary (.h .cpp)
StreamDoNothing (.h .cpp)
StringConvert (.h .cpp)
The following files belong to the UD-application:
ByteStreamT (.cpp)
Complex_N (.h .cpp)
Complex_R (.h .cpp)
Date_N (.h .cpp)
Date_R (.h .cpp)
Simple_N (.h .cpp)
Simple_R (.h .cpp)
Final MyStream
The example of MyStream has been shown already in the chapter about templates.
Its advantages are:
- generally applicable
- it acts STL-stringstream-like without having its implications
- you haven’t to deal with spaces in strings or empty strings it solves that problem for you
- persisted files are human-readable
Final MyStream consists of the following files:
CMyStream (.h .cpp)
StreamDoNothing (.h .cpp)
The following files belong to the UD-application:
CArticle (.h .cpp)
COrder (.h .cpp)
CPerson (.h .cpp)
CShipAdvanced (.h .cpp)
CShippment (.h .cpp)
CTransaction (.h .cpp)
MyStream (.cpp)
Final StringRstream
StringRstream is an exemplary implementation that shows how to stream managed objects and System:: types with STL-streams.
It demonstrates the technique and has all essential elements.
Final StringRstream consists of the following files:
RStream (.h .cpp)
StreamDoNothing (.h .cpp)
StringConvert (.h .cpp)
StrString (.h .cpp)
The following files belong to the UD-application:
CArticle (.h .cpp)
COrder (.h .cpp)
CPerson (.h .cpp)
CShippment (.h .cpp)
CTransaction (.h .cpp)
StringRstream (.cpp)
I wish you much fun and success with streaming all around.
;-))
Feedback
Please let me know your experience with any of the provided articles and sources, or any questions about.
And it would be interesting if other compilers have implemented the same or different behavior with STL-streams.
Sources
Binary Stream
T2_Bytes1
| The principals of class Bytes (= my Binary-Stream)
|
declares + implements the basics of class Bytes
main() is a rich commented example of using the interface of Bytes.
|
T3_Object1
| Bytes-Stream with objects,
implementing the common pattern of having the stream as friend of the class.
|
declares + implements a UD-class with it's interface to class Bytes.
Class Bytes is moved to seperate header and implementation.
main() shows the usage.
|
T4_Bytes2
| Bytes-Stream with managed Ref-Objects,
implementing the alternative pattern without having the stream as friend of the class.
|
declares + implements a UD-managed ref-class with its interface to class Bytes.
Because it's impossible for managed classes to have a friend-declaration, an alternative pattern is implemented for the cooperation with a stream.
The details to the alternative pattern are shown in chapter "alternative Pattern".
|
Interface for classes
BytesInterface
| How Bytes works
Bytes as a datagram of an object
|
that implements the full interoperation of standard- and managed UD-classes.
Bytes-Stream gets two new interface-classes BinaryX and IXbinary.
BinaryX does the writing and reading of data-members,
IXbinary provides the interface that UD-class needs to Bytes-Stream, that can be inherited by a UD-class.
|
Streaming all around
ByteStream
| Bytes with StringStream-Backbone
a Set of object-datagrams
|
implements an interface of Bytes-Stream to STL-StringStream, to get a Set of object-datagrams
|
alternate Pattern
T1_pattern
| Ostream 'wcout' for managed types
|
explains the necessity of the pattern and demonstrates its implementation.
|
Ostream
| New pattern with Ostream; Classes with Inner-Classes
|
exemplary implementation of the pattern with nested classes
|
Stringstream
| New pattern with StringStream
|
exemplary implementation of the pattern for output- and input-stream StringStream.
|
demonstrateStr
| Syntax pitfall
|
demonstration of 'do nothing', or not?
|
TraditionalStream
| Traditional pattern with StringStream
|
an implementation of example "Stringstream" with the traditional pattern.
|
Syntax of STL-Stream effects or defects?
demonstrateStr
| Syntax pitfall
|
the missing link in Stream-Syntax?
|
LeftToRight
| Exploring the left to right associativity of different Streams.
Syntax exploration, pitfalls
|
Left to Right isn't allways left-to-right.
To get the full experience, see the article.
|
OstrFormatter
| Syntax exploration
|
an example about methods in stream-statements
|
Manipulator
| Stream Manipulator for Native & Managed Code
|
manipulates its parameter before output and past input.
Implemented in StrString(.h .cpp)
|
Own Streams
CMyQstr(.h .cpp)
| in project LeftAndRight
|
simple Example with the core-elements a stream needs.
|
MyStreamX
| a Stream develloped on one's own
|
Exercise with Basic-Types and nested classes
shows a technique to serialize Queues or arrays.
shows a general way to persistence via STL-StringStream and STL-FileStream.
|
Template
TemplateFoo
| Example: Function‑Template
|
a Function‑Template, Template‑Specialization, Template‑Overloading
|
MyStream
| Queue as StringStream-like implementation
|
works StringStream-like without the StringStream-complications,
generalized by template-methods,
|
QueueTemplate
| Typesafe Queue-Stream for all std‑types and UD‑classes
|
implements a typesafe Queue-Stream.
Exemplary implementation of a template-class with spezialisation.
|
Wrapper, Template
_Common
| Bytes & Interfaces
& template-class as wrapper of IN-/IRbinary
ByteStreamT uses that templated interface.
|
implements an interface wrapper-class template, that takes a reference of UD-class.
It has least implications for the design of UD-class.
|
Finals
ByteStream
| BytesQueue; uses the templated interface _Common
|
Bin-Queue as container for UD-Classes
a stream that is a Set of binary Object-Datagrams.
For all standard and managed basic-types and UD-classes.
With a generalized interface to STL-Stream for persistence.
|
MyStream
| Queue as StringStream-like implementation
|
a container for all standard and managed basic-types and UD-classes.
With a generalized interface to STL-Stream for persistence.
Generally applicable, files are human-readable.
|
StringRstream
| Managed objects and System::Types streamed to and from STL-StringStream ;-D
|
Even managed types and objects are streamable with STL-Streams.
|