This is a bit over my head but I do not have a problem reading about it and trying to learn this. However what's the benefits of me doing it this way?
Just to add some of my own commentary in addition to the points hplus already mentioned:
To put simply, rather than having to write 4 pieces of logic: stream writer, stream reader, object writer, object reader, you will only have to write 3: stream writer, stream reader, object visitor. It might not seem like much at first, but as your project gets larger, the difference between the two methods' amount of code is huge. In addition, when you use the object writer and object reader approach, you have two pieces of logic you have to maintain and keep track up for maintenance whereas the object visitor is only one piece of logic.
By going the object visitor route, you further decrease development time when you have base types that can be reused. For example, let's say you have 5 different messages that all contain the same sequence of data, like entity id, X, Y, Z. In the object reader/writer approach, you will simply have logic to read/write each of those fields individually in all of your functions. In the object visitor approach, if you combined those 4 fields into a base type, you would only have to write the visit function logic once for that type, then reap the benefits of being able to reuse it for any message that uses it. So rather than having 8 lines of actual reading/writing code total, you only have 1. That is because the visitor pattern handles both reading and writing!
Here is a real world example. Consider the following structure that contains data about a security protocol:
[spoiler]
struct Data_Security
{
unsigned char mode;
unsigned __int64 initial_key;
unsigned int seed_count;
unsigned int crc_seed;
unsigned __int64 handshake_key;
unsigned int g;
unsigned int p;
unsigned int A;
Data_Security()
{
mode = 0;
initial_key = 0;
seed_count = 0;
crc_seed = 0;
handshake_key = 0;
g = 0;
p = 0;
A = 0;
}
};
[/spoiler]
Using the object writer/reader approach, we would have the following two functions:
[spoiler]
Data_Security FromStream( StreamReader stream )
{
Data_Security object;
object.mode = stream.ReadUInt8();
if( object.mode & 2 )
{
object.initial_key = stream.ReadUInt64();
}
if( object.mode & 4 )
{
object.seed_count = stream.ReadUInt32();
object.crc_seed = stream.ReadUInt32();
}
if( object.mode & 8 )
{
object.handshake_key = stream.ReadUInt64();
object.g = stream.ReadUInt32();
object.p = stream.ReadUInt32();
object.A = stream.ReadUInt32();
}
if( object.mode & 16 )
{
object.handshake_key = stream.ReadUInt64();
}
}
void ToStream( StreamWriter stream, const Data_Security & object )
{
stream.WriteUInt8( object.mode );
if( object.mode & 2 )
{
stream.WriteUInt64( object.initial_key );
}
if( object.mode & 4 )
{
stream.WriteUInt32( object.seed_count );
stream.WriteUInt32( object.crc_seed );
}
if( object.mode & 8 )
{
stream.WriteUInt64( object.handshake_key );
stream.WriteUInt32( object.g );
stream.WriteUInt32( object.p );
stream.WriteUInt32( object.A );
}
if( object.mode & 16 )
{
stream.WriteUInt64( object.handshake_key );
}
}
[/spoiler]
Where the WriteXXX / ReadXXX functions are coded as part of the StreamWriter / StreamReader class.
Now, for the object visitor pattern, we only have one function:
[spoiler]
template< typename Stream >
Stream & visit( Data_Security & value, Stream & stream )
{
stream.visit( "mode", value.mode );
if( value.mode & 2 )
{
stream.visit( "initial_key", value.initial_key );
}
if( value.mode & 4 )
{
stream.visit( "seed_count", value.seed_count );
stream.visit( "crc_seed", value.crc_seed );
}
if( value.mode & 8 )
{
stream.visit( "handshake_key", value.handshake_key );
stream.visit( "g", value.g );
stream.visit( "p", value.p );
stream.visit( "A", value.A );
}
if( value.mode & 16 )
{
stream.visit( "handshake_key", value.handshake_key );
}
return stream;
}
[/spoiler]
We still have both StreamReader and StreamWriter classes, but rather than them defining Read/Write named functions, they all use the same "visit" function with different logic depending on the object.
So we are taking advantage of the way C++ works to drastically cut down on the work and code needed to implement object serialization. The more types you have, the more visit functions you do have to write, but you only have to write them once, so you can easily reuse them in the future. As mentioned before, as your project grows, the object visitor pattern pays for itself.
The object reader/writer code shown is typically how you see people do it. I myself used that style for years because I was unaware of the visitor pattern. Now that I understand it better, I can see how beneficial it is and how there really is no reason to use the object reader/writer method because everything you can accomplish there, you can accomplish with the visitor pattern; you just might need to add a state object to know some extra information.
Here's some simple examples of more complete visitor stream classes shown in hplus's earlier post:
SeralizeStream
[spoiler]
class SeralizeStream
{
private:
std::vector< unsigned char > m_buffer;
public:
SeralizeStream & visit( const std::string & name, const std::string & value )
{
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << std::endl;
#endif
size_t size = value.size();
do
{
if( size >= 255 )
{
m_buffer.push_back( 255 );
size -= 254;
}
else
{
m_buffer.push_back( static_cast< unsigned char >( size ) );
size -= size;
}
} while( size != 0 );
m_buffer.insert( m_buffer.end(), value.begin(), value.end() );
return *this;
}
SeralizeStream & visit( const std::string & name, const unsigned char & value )
{
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << (int)value << " (0x" << std::hex << (int)value << std::dec << ")" << std::endl;
#endif
m_buffer.push_back( value );
return *this;
}
SeralizeStream & visit( const std::string & name, const signed char & value )
{
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << (int)value << " (0x" << std::hex << (int)value << std::dec << ")" << std::endl;
#endif
m_buffer.push_back( value );
return *this;
}
SeralizeStream & visit( const std::string & name, const unsigned short & value )
{
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << " (0x" << std::hex << value << std::dec << ")" << std::endl;
#endif
#if ENDIAN_BIG == 1
m_buffer.push_back( ( value >> 8 ) & 0xFF );
m_buffer.push_back( ( value >> 0 ) & 0xFF );
#elif ENDIAN_LITTLE == 1
m_buffer.push_back( ( value >> 0 ) & 0xFF );
m_buffer.push_back( ( value >> 8 ) & 0xFF );
#endif
return *this;
}
SeralizeStream & visit( const std::string & name, const signed short & value )
{
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << " (0x" << std::hex << value << std::dec << ")" << std::endl;
#endif
#if ENDIAN_BIG == 1
m_buffer.push_back( ( value >> 8 ) & 0xFF );
m_buffer.push_back( ( value >> 0 ) & 0xFF );
#elif ENDIAN_LITTLE == 1
m_buffer.push_back( ( value >> 0 ) & 0xFF );
m_buffer.push_back( ( value >> 8 ) & 0xFF );
#endif
return *this;
}
SeralizeStream & visit( const std::string & name, const unsigned int & value )
{
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << " (0x" << std::hex << value << std::dec << ")" << std::endl;
#endif
#if ENDIAN_BIG == 1
m_buffer.push_back( ( value >> 24 ) & 0xFF );
m_buffer.push_back( ( value >> 16 ) & 0xFF );
m_buffer.push_back( ( value >> 8 ) & 0xFF );
m_buffer.push_back( ( value >> 0 ) & 0xFF );
#elif ENDIAN_LITTLE == 1
m_buffer.push_back( ( value >> 0 ) & 0xFF );
m_buffer.push_back( ( value >> 8 ) & 0xFF );
m_buffer.push_back( ( value >> 16 ) & 0xFF );
m_buffer.push_back( ( value >> 24 ) & 0xFF );
#endif
return *this;
}
SeralizeStream & visit( const std::string & name, const signed int & value )
{
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << " (0x" << std::hex << value << std::dec << ")" << std::endl;
#endif
#if ENDIAN_BIG == 1
m_buffer.push_back( ( value >> 24 ) & 0xFF );
m_buffer.push_back( ( value >> 16 ) & 0xFF );
m_buffer.push_back( ( value >> 8 ) & 0xFF );
m_buffer.push_back( ( value >> 0 ) & 0xFF );
#elif ENDIAN_LITTLE == 1
m_buffer.push_back( ( value >> 0 ) & 0xFF );
m_buffer.push_back( ( value >> 8 ) & 0xFF );
m_buffer.push_back( ( value >> 16 ) & 0xFF );
m_buffer.push_back( ( value >> 24 ) & 0xFF );
#endif
return *this;
}
SeralizeStream & visit( const std::string & name, const unsigned __int64 & value )
{
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << " (0x" << std::hex << value << std::dec << ")" << std::endl;
#endif
#if ENDIAN_BIG == 1
m_buffer.push_back( ( value >> 56 ) & 0xFF );
m_buffer.push_back( ( value >> 48 ) & 0xFF );
m_buffer.push_back( ( value >> 40 ) & 0xFF );
m_buffer.push_back( ( value >> 32 ) & 0xFF );
m_buffer.push_back( ( value >> 24 ) & 0xFF );
m_buffer.push_back( ( value >> 16 ) & 0xFF );
m_buffer.push_back( ( value >> 8 ) & 0xFF );
m_buffer.push_back( ( value >> 0 ) & 0xFF );
#elif ENDIAN_LITTLE == 1
m_buffer.push_back( ( value >> 0 ) & 0xFF );
m_buffer.push_back( ( value >> 8 ) & 0xFF );
m_buffer.push_back( ( value >> 16 ) & 0xFF );
m_buffer.push_back( ( value >> 24 ) & 0xFF );
m_buffer.push_back( ( value >> 32 ) & 0xFF );
m_buffer.push_back( ( value >> 40 ) & 0xFF );
m_buffer.push_back( ( value >> 48 ) & 0xFF );
m_buffer.push_back( ( value >> 56 ) & 0xFF );
#endif
return *this;
}
SeralizeStream & visit( const std::string & name, const signed __int64 & value )
{
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << " (0x" << std::hex << value << std::dec << ")" << std::endl;
#endif
#if ENDIAN_BIG == 1
m_buffer.push_back( ( value >> 56 ) & 0xFF );
m_buffer.push_back( ( value >> 48 ) & 0xFF );
m_buffer.push_back( ( value >> 40 ) & 0xFF );
m_buffer.push_back( ( value >> 32 ) & 0xFF );
m_buffer.push_back( ( value >> 24 ) & 0xFF );
m_buffer.push_back( ( value >> 16 ) & 0xFF );
m_buffer.push_back( ( value >> 8 ) & 0xFF );
m_buffer.push_back( ( value >> 0 ) & 0xFF );
#elif ENDIAN_LITTLE == 1
m_buffer.push_back( ( value >> 0 ) & 0xFF );
m_buffer.push_back( ( value >> 8 ) & 0xFF );
m_buffer.push_back( ( value >> 16 ) & 0xFF );
m_buffer.push_back( ( value >> 24 ) & 0xFF );
m_buffer.push_back( ( value >> 32 ) & 0xFF );
m_buffer.push_back( ( value >> 40 ) & 0xFF );
m_buffer.push_back( ( value >> 48 ) & 0xFF );
m_buffer.push_back( ( value >> 56 ) & 0xFF );
#endif
return *this;
}
SeralizeStream & visit( const std::string & name, const float & value )
{
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << std::endl;
#endif
return visit( name, *( unsigned int * )( &value ) );
}
SeralizeStream & visit( const std::string & name, const double & value )
{
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << std::endl;
#endif
return visit( name, *( unsigned __int64 * )( &value ) );
}
std::vector< unsigned char > & Buffer()
{
return m_buffer;
}
void Clear()
{
m_buffer.clear();
}
};
[/spoiler]
DeseralizeStream
[spoiler]
class DeseralizeStream
{
private:
std::vector<unsigned char> & m_buffer;
size_t m_index;
public:
DeseralizeStream(std::vector<unsigned char> & buffer)
: m_buffer( buffer ), m_index( 0 )
{
}
DeseralizeStream & visit( const std::string & name, std::string & value )
{
size_t size = 0;
while( m_buffer[ m_index ] == 255 )
{
size += 254;
++m_index;
}
size += m_buffer[ m_index++ ];
value.resize( size );
std::copy( m_buffer.begin() + m_index, m_buffer.begin() + m_index + size, value.begin() );
m_index += size;
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << std::endl;
#endif
return *this;
}
DeseralizeStream & visit( const std::string & name, unsigned char & value )
{
value = (unsigned char)m_buffer[ m_index++ ];
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << (int)value << " (0x" << std::hex << (int)value << std::dec << ")" << std::endl;
#endif
return *this;
}
DeseralizeStream & visit( const std::string & name, signed char & value )
{
value = (signed char)m_buffer[ m_index++ ];
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << (int)value << " (0x" << std::hex << (int)value << std::dec << ")" << std::endl;
#endif
return *this;
}
DeseralizeStream & visit( const std::string & name, unsigned short & value )
{
#if ENDIAN_BIG == 1
value = (unsigned short)m_buffer[ m_index++ ] << 8 | (unsigned short)m_buffer[ m_index++ ] << 0;
#elif ENDIAN_LITTLE == 1
value = (unsigned short)m_buffer[ m_index++ ] << 0 | (unsigned short)m_buffer[ m_index++ ] << 8;
#endif
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << " (0x" << std::hex << value << std::dec << ")" << std::endl;
#endif
return *this;
}
DeseralizeStream & visit( const std::string & name, signed short & value )
{
#if ENDIAN_BIG == 1
value = (signed short)m_buffer[ m_index++ ] << 8 | (signed short)m_buffer[ m_index++ ] << 0;
#elif ENDIAN_LITTLE == 1
value = (signed short)m_buffer[ m_index++ ] << 0 | (signed short)m_buffer[ m_index++ ] << 8;
#endif
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << " (0x" << std::hex << value << std::dec << ")" << std::endl;
#endif
return *this;
}
DeseralizeStream & visit( const std::string & name, unsigned int & value )
{
#if ENDIAN_BIG == 1
value = (unsigned int)m_buffer[ m_index++ ] << 24 | (unsigned int)m_buffer[ m_index++ ] << 16 | (unsigned int)m_buffer[ m_index++ ] << 8 | (unsigned int)m_buffer[ m_index++ ] << 0;
#elif ENDIAN_LITTLE == 1
value = (unsigned int)m_buffer[ m_index++ ] << 0 | (unsigned int)m_buffer[ m_index++ ] << 8 | (unsigned int)m_buffer[ m_index++ ] << 16 | (unsigned int)m_buffer[ m_index++ ] << 24;
#endif
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << " (0x" << std::hex << value << std::dec << ")" << std::endl;
#endif
return *this;
}
DeseralizeStream & visit( const std::string & name, signed int & value )
{
#if ENDIAN_BIG == 1
value = (signed int)m_buffer[ m_index++ ] << 24 | (signed int)m_buffer[ m_index++ ] << 16 | (signed int)m_buffer[ m_index++ ] << 8 | (signed int)m_buffer[ m_index++ ] << 0;
#elif ENDIAN_LITTLE == 1
value = (signed int)m_buffer[ m_index++ ] << 0 | (signed int)m_buffer[ m_index++ ] << 8 | (signed int)m_buffer[ m_index++ ] << 16 | (signed int)m_buffer[ m_index++ ] << 24;
#endif
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << " (0x" << std::hex << value << std::dec << ")" << std::endl;
#endif
return *this;
}
DeseralizeStream & visit( const std::string & name, unsigned __int64 & value )
{
#if ENDIAN_BIG == 1
value = (unsigned __int64)m_buffer[ m_index++ ] << 56 | (unsigned __int64)m_buffer[ m_index++ ] << 48 | (unsigned __int64)m_buffer[ m_index++ ] << 40 | (unsigned __int64)m_buffer[ m_index++ ] << 32 | (unsigned __int64)m_buffer[ m_index++ ] << 24 | (unsigned __int64)m_buffer[ m_index++ ] << 16 | (unsigned __int64)m_buffer[ m_index++ ] << 8 | (unsigned __int64)m_buffer[ m_index++ ] << 0;
#elif ENDIAN_LITTLE == 1
value = (unsigned __int64)m_buffer[ m_index++ ] << 0 | (unsigned __int64)m_buffer[ m_index++ ] << 8 | (unsigned __int64)m_buffer[ m_index++ ] << 16 | (unsigned __int64)m_buffer[ m_index++ ] << 24 | (unsigned __int64)m_buffer[ m_index++ ] << 32 | (unsigned __int64)m_buffer[ m_index++ ] << 40 | (unsigned __int64)m_buffer[ m_index++ ] << 48 | (unsigned __int64)m_buffer[ m_index++ ] << 56;
#endif
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << " (0x" << std::hex << value << std::dec << ")" << std::endl;
#endif
return *this;
}
DeseralizeStream & visit( const std::string & name, signed __int64 & value )
{
#if ENDIAN_BIG == 1
value = (signed __int64)m_buffer[ m_index++ ] << 56 | (signed __int64)m_buffer[ m_index++ ] << 48 | (signed __int64)m_buffer[ m_index++ ] << 40 | (signed __int64)m_buffer[ m_index++ ] << 32 | (signed __int64)m_buffer[ m_index++ ] << 24 | (signed __int64)m_buffer[ m_index++ ] << 16 | (signed __int64)m_buffer[ m_index++ ] << 8 | (signed __int64)m_buffer[ m_index++ ] << 0;
#elif ENDIAN_LITTLE == 1
value = (signed __int64)m_buffer[ m_index++ ] << 0 | (signed __int64)m_buffer[ m_index++ ] << 8 | (signed __int64)m_buffer[ m_index++ ] << 16 | (signed __int64)m_buffer[ m_index++ ] << 24 | (signed __int64)m_buffer[ m_index++ ] << 32 | (signed __int64)m_buffer[ m_index++ ] << 40 | (signed __int64)m_buffer[ m_index++ ] << 48 | (signed __int64)m_buffer[ m_index++ ] << 56;
#endif
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << " (0x" << std::hex << value << std::dec << ")" << std::endl;
#endif
return *this;
}
DeseralizeStream & visit( const std::string & name, float & value )
{
unsigned int v;
visit( name, v );
memcpy( &value, &v, 4 );
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << std::endl;
#endif
return *this;
}
DeseralizeStream & visit( const std::string & name, double & value )
{
unsigned __int64 v;
visit( name, v );
memcpy( &value, &v, 8 );
#ifdef _DEBUG
std::cout << "[" << __FUNCTION__<< "] " << name << " => " << value << std::endl;
#endif
return *this;
}
};
[/spoiler]
They are pretty basic classes. More functions for vectors, lists, maps, etc... could be added as needed. Also the way you work with strings might vary. Some protocols use fixed size strings, some use a variable length variable size type similar to the one shown, and others just use a variable length fixed size type (as shown in hplus's post with a 1 byte length limitation). I think the float/double logic is correct, but I might be wrong. Another thing to be careful of is ensuring you are using portable types (I'm not purposefully for the sake of a simple test). There are a few gotchas here you have to be careful of if you are going cross-platform or 32/64-bit different architectures. The most annoying one is the differences between wchar_t size on gcc on linux (4 bytes usually) and the size on windows (2 bytes usually). If you tried to send a string from one platform to the other without keeping this in mind, you can be in for some real headaches!(I.e. Windows Client <-> Linux Proxy Server <-> Windows Server).
Anyways, hopefully that adds to the useful information in this thread. Also as a disclaimer, all code was written during the course of reading this thread, so it might have bugs, do not use it without understanding what it does first. Good luck!