Rule of three and so forth...

F

Frederick Gotham

(This sounds like a tutorial at the start, but it's actually a question
when you get down into it...)

When writing slightly more elaborate classes, there are Things To Be Aware
Of.

A good example is "The Rule of Three". Basically, if you acquire a resource
in the constructor, then you're going to need to write a special copy-
constructor and assignment operator, e.g.:

(Before I begin, I'm going to write a small template in order to help me
default-initialise a dynamically allocated array -- functionality which is
lacking is C++.)

template<class T, unsigned long len>
struct DefInitArray {

T array[len];

DefInitArray() : array() {}
};



Now here's my code:


#include <cstring>

class ArbitraryClass {
protected:

static unsigned long const buflen = 1024;

char * const p_buf;

public:

ArbitraryClass() : p_buf( (new DefInitArray<char,buflen>[1])->array )
{}

ArbitraryClass(const ArbitraryClass &original) : p_buf(new char
[buflen])
{
std::memcpy(p_buf, original.p_buf, sizeof *p_buf * buflen);
}

ArbitraryClass &operator=(const ArbitraryClass &rhs)
{
std::memcpy(p_buf, rhs.p_buf, sizeof *p_buf * buflen);
}

~ArbitraryClass()
{
delete [] p_buf;
}

};

int main()
{
ArbitraryClass a;

ArbitraryClass b( a );

a = b;
}


Expanding on this, I'm going to rename the class to "StringStream", and
then I'm going to add five things to it:

(1) A pointer to the current position in the buffer called "pos".
(2) Code in the constructor, copy-constructor and assignment operator
which correctly sets the value of "pos".
(3) An operator<< in order to write to the stream.
(4) An IsFull() member function to test whether the buffer is full.
(5) A "Print" member function to print the entire buffer contents.

Here's the updated code:

template<class T, unsigned long len>
struct DefInitArray {

T array[len];

DefInitArray() : array() {}

};

#include <cstring>
#include <ostream>
#include <cstdlib>
#include <iostream>

class StringStream {
protected:

static unsigned long const buflen = 1024;

char * const p_buf;

char *pos;

public:

StringStream() :
p_buf( (new DefInitArray<char,buflen>[1])->array ),
pos( p_buf )
{}

StringStream(const StringStream &original) :
p_buf(new char[buflen]),
pos( p_buf + (original.pos - original.p_buf) )
{
std::memcpy(p_buf, original.p_buf, sizeof *p_buf * buflen);
}

StringStream &operator=(const StringStream &rhs)
{
std::memcpy(p_buf, rhs.p_buf, sizeof *p_buf * buflen);

pos = p_buf + (rhs.pos - rhs.p_buf);
}

bool IsFull() const
{
return pos == p_buf + (buflen - 1);
}

StringStream &operator<<( char const c )
{
if( !IsFull() ) *pos++ = c;

return *this;
}

StringStream &operator<<( const char *p )
{
for( ; *p; ++p) *this << *p;
}

void Print( std::eek:stream &out ) const
{
out << p_buf << '\n';
}

~StringStream()
{
delete [] p_buf;
}

};


int main()
{
StringStream a;

a << "Hello, my name's Fred";

a.Print( std::cout );


StringStream b( a );

b << ", and I'm a clone!";

b.Print( std::cout );


a = b;

a.Print( std::cout );


std::system("PAUSE");
}



Okay so my code works fine, so what's missing?

What happens if a StringStream object gets re-located in memory (perhaps by
a swap)? If it were to be re-located, its "pos" member would effectively
become corrupt.

So my question is:

What are the Thing To Be Aware Of when storing an object's address
within itself (or the address of a member object, or base class, etc.)?


Secondly:

What exactly do I have to do to my StringStream class in order for it
to survive a relocation? Would I be right in thinking I've to do something
like:

namespace std {
void swap( StringStream&, StringStream& );
}
 
F

Frederick Gotham

Frederick Gotham posted:

StringStream &operator<<( const char *p )
{
for( ; *p; ++p) *this << *p;
}


Wups! I'm missing:

return *this;


Does anyone else think it's RIDICULOUS that g++ doesn't catch that error?!
 
B

Bernd Strieder

Hello,

Frederick said:
Wups! I'm missing:

return *this;


Does anyone else think it's RIDICULOUS that g++ doesn't catch that
error?!
If you put warnings on with -Wall it will give notice.

Bernd Strieder
 
B

Bernd Strieder

Hello,

Frederick said:
What happens if a StringStream object gets re-located in memory
(perhaps by a swap)? If it were to be re-located, its "pos" member
would effectively become corrupt.

A swap is by default defined using copy c'tor and assignments, that
should not be critical. A specialized variant of swap will use swap on
the data members of the class. Either way by swapping the buffer and
the pos will be swapped, and everything is fine. So you must be
thinking about relocation by moving raw memory, which leads immediately
to undefined behaviour on non-POD data types, no surprise, no need to
consider this.
So my question is:

What are the Thing To Be Aware Of when storing an object's address
within itself (or the address of a member object, or base class,
etc.)?

Just the rule of three. Every sensible way of relocation should be
possible using the three from the rule of three. Other code with direct
access to the members could do it its own way. All relocating code has
the responsibility to do it right, or you miss the invariants of the
class and it will fail.
Secondly:

What exactly do I have to do to my StringStream class in order for
it
to survive a relocation? Would I be right in thinking I've to do
something like:

namespace std {
void swap( StringStream&, StringStream& );
}

Swap is not ciritical, as said before, can be done right. Logically, a
stream class is not a candidate for relocation, anyway.

Bernd Strieder
 
F

Frederick Gotham

Bernd Strieder posted:
Hello,



A swap is by default defined using copy c'tor and assignments, that
should not be critical.


I'm surprised to hear that! I thought (and hoped) that it would be more
efficient, something like:

#include <cstring>

template<class T>
void swap( T &a, T &b )
{
unsigned char buffer[ sizeof(T) ];

std::memcpy( buffer, &a, sizeof(T) );

std::memcpy( &a, &b, sizeof(T) );

std::memcpy( &b, buffer, sizeof(T) );
}


It's for this reason that I thought it was the class implementor's own
responsibility to implement their own swap algorithm if the object stores
its own address within itself.

I might do so anyway, for sake of efficiency.

How might I define it? Should I define it within the std namespace? How
does the following look:

template<class T, unsigned long len>
struct DefInitArray {

T array[len];

DefInitArray() : array() {}

};

#include <cstring>
#include <ostream>
#include <cstdlib>
#include <iostream>

class StringStream;

namespace std { void swap(StringStream&, StringStream&); }

class StringStream {
protected:

static unsigned long const buflen = 1024;

char * const p_buf;

char *pos;

public:

friend void std::swap(StringStream&,StringStream&);

StringStream() :
p_buf( (new DefInitArray<char,buflen>[1])->array ),
pos( p_buf )
{}

StringStream(const StringStream &original) :
p_buf(new char[buflen]),
pos( p_buf + (original.pos - original.p_buf) )
{
std::memcpy(p_buf, original.p_buf, sizeof *p_buf * buflen);
}

StringStream &operator=(const StringStream &rhs)
{
std::memcpy(p_buf, rhs.p_buf, sizeof *p_buf * buflen);

pos = p_buf + (rhs.pos - rhs.p_buf);

return *this;
}

bool IsFull() const
{
return pos == p_buf + (buflen - 1);
}

StringStream &operator<<( char const c )
{
if( !IsFull() ) *pos++ = c;

return *this;
}

StringStream &operator<<( const char *p )
{
for( ; *p; ++p) *this << *p;

return *this;
}

void Print( std::eek:stream &out ) const
{
out << p_buf << '\n';
}

~StringStream()
{
delete [] p_buf;
}

};


namespace std {

void swap( StringStream &a, StringStream &b )
{
if ( &a == &b ) return; /* Is this check redundant? */

if ( &b > &a )
{
b.pos -= &b - &a;
a.pos += &b - &a;
}
else
{
a.pos -= &a - &b;
b.pos += &a - &b;
}

unsigned char temp[ StringStream::buflen ];

std::memcpy( temp, a.p_buf, StringStream::buflen );
std::memcpy( a.p_buf, b.p_buf, StringStream::buflen );
std::memcpy( b.p_buf, temp, StringStream::buflen );
}

}

int main()
{
StringStream a;

a << "Hello, my name's Fred";

a.Print( std::cout );


StringStream b( a );

b << ", and I'm a clone!";

b.Print( std::cout );


a = b;

a.Print( std::cout );


std::swap( a, b );


std::system("PAUSE");
}
 
R

Roland Pibinger

(This sounds like a tutorial at the start, but it's actually a question
when you get down into it...)
When writing slightly more elaborate classes, there are Things To Be Aware
Of.
A good example is "The Rule of Three". Basically, if you acquire a resource
in the constructor, then you're going to need to write a special copy-
constructor and assignment operator, e.g.:

Not really ...
class StringStream {
protected:
static unsigned long const buflen = 1024;
char * const p_buf;
char *pos;
public:
StringStream() :
p_buf( (new DefInitArray<char,buflen>[1])->array ),
pos( p_buf )
{}

StringStream(const StringStream &original) :
p_buf(new char[buflen]),
pos( p_buf + (original.pos - original.p_buf) )
{
std::memcpy(p_buf, original.p_buf, sizeof *p_buf * buflen);
}

StringStream &operator=(const StringStream &rhs)
{
std::memcpy(p_buf, rhs.p_buf, sizeof *p_buf * buflen);

pos = p_buf + (rhs.pos - rhs.p_buf);
} ....
};

Your StringStream class is a typical example for a class that should
be non-copyable (see also std::stingstream). Just make your copy
constructor and operator= private and leave them unimplemented.

Best wishes,
Roland Pibinger
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,054
Latest member
TrimKetoBoost

Latest Threads

Top