Rule of three and so forth...

Discussion in 'C++' started by Frederick Gotham, Jun 28, 2006.

  1. (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& );
    }


    --

    Frederick Gotham
     
    Frederick Gotham, Jun 28, 2006
    #1
    1. Advertising

  2. 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?!


    --

    Frederick Gotham
     
    Frederick Gotham, Jun 28, 2006
    #2
    1. Advertising

  3. Hello,

    Frederick Gotham wrote:
    > 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
     
    Bernd Strieder, Jun 28, 2006
    #3
  4. Hello,

    Frederick Gotham wrote:

    > 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
     
    Bernd Strieder, Jun 28, 2006
    #4
  5. Bernd Strieder posted:

    > Hello,
    >
    > Frederick Gotham wrote:
    >
    >> 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.



    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");
    }



    --

    Frederick Gotham
     
    Frederick Gotham, Jun 28, 2006
    #5
  6. On Wed, 28 Jun 2006 11:04:56 GMT, Frederick Gotham
    <> wrote:
    >(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
     
    Roland Pibinger, Jun 28, 2006
    #6
    1. Advertising

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

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. John Benson
    Replies:
    4
    Views:
    740
    David M. Cooke
    Jan 9, 2004
  2. Replies:
    0
    Views:
    1,415
  3. SpreadTooThin
    Replies:
    10
    Views:
    474
    Gennaro Prota
    Jun 15, 2007
  4. utab
    Replies:
    4
    Views:
    547
    Ron Natalie
    Feb 7, 2008
  5. Matt
    Replies:
    0
    Views:
    209
Loading...

Share This Page