Exception safe at what cost

Collapse
This topic is closed.
X
X
 
  • Time
  • Show
Clear All
new posts
  • Steven T. Hatton

    Exception safe at what cost

    I read Stroustrup's article of the day:


    Programming with Exceptions. InformIt.com. April 2001.


    Some of these ideas are finally beginning to sink in. I believe I looked at
    the same article a while back and decided I wasn't quite ready for it. If I
    understood things correctly, there seems to be a slight problem with the
    design of his exception safe assignment operator. I believe I have
    correctly extracted all the relevant code, and appropriately commented it
    to describe the various issues discussed in the article. My questions are
    in the final comment block.

    class Vector { // v points to an array of sz ints
    int sz;
    int* v;
    public: explicit Vector(int n); // create vector of n ints
    Vector(const Vector&);
    ~Vector(); // destroy vector
    Vector& operator=(const Vector&); // assignment
    int size() const;
    void resize(int n); // change the size to n
    int& operator[](int i); // subscripting
    const int& operator[](int i) const; // subscripting
    };

    Vector::Vector( int i) //constructor
    :sz(i) ,v(new int[i]) { } // establishes class invariant

    Vector::~Vector () { delete[] v; }

    int Vector::size() const { return sz; } //no effect on class representation

    struct Bad_range { }; // I want an Über-exception to derive from!

    int& Vector::operato r[](int i)
    {
    if (0<=i && i<sz) return v[i];
    throw Bad_range(); // no effect on class representation if thrown
    }


    /*
    This is a bad assignment operator implementation because
    it can lead to bad things like double deletion when exceptions
    are thrown. That's because it fails to maintain the class invariant
    that says a Vector holds an array

    */
    Vector& Vector::operato r=(const Vector& a)
    {
    sz = a.sz; // get new size
    delete[] v; // free old memory
    v = new int[n]; // get new memory
    copy(a.v,a.v+a. sz,v); // copy to new memory
    }

    /*
    The is a better version because it maintains the invariant
    even if the memory allocation fails. We can probably trust
    copy() not to throw an exception, or to otherwise fail,
    so we don't worry about losing *p if it fails.
    */
    Vector& Vector::operato r=(const Vector& a)
    {
    int* p = new int[n]; // get new memory [or thorw bad_alloc?]
    copy(a.v,a.v+a. sz,p); // copy to new memory

    /* invariant is violated in the next statement*/
    sz = a.sz; // get new size
    delete[] v; // free old memory

    /* invariant is reestablished in the next statement*/
    v = p;
    }

    /*
    This is the example of where the problem with the first form
    of Vector::operato r=(const Vector& a) might arise. Vector::v is
    deleted once by the assignment operator, and then again when the
    destructor Vector::~Vector () is called upon exit of the containing
    block.
    */
    int main() {
    try
    {
    Vector vec(10) ;
    cout << vec.size() << ´\n´; // so far, so good
    Vector v2(40*1000000) ; // ask for 160 megabytes
    vec = v2; // use another 160 megabytes
    }
    catch(Range_err or) {
    cerr << "Oops: Range error!\n";
    }
    catch(bad_alloc ) {
    cerr << "Oops: memory exhausted!\n";
    }
    }

    /*
    Now my observation is this:

    If I don't free the memory in Vector::v until the penultimate statement
    in the assignment operator function, then I will not be able to
    allocate as much memory as I would with the original (bad)
    implementation.

    Earlier in the article Stroustrup provides this example of a
    destructor: ~File_ptr() { if (p) fclose(p); }

    If I were to use the comparable ~Vector() { if(v) delete[] v; }
    in conjunction with the first form of the assignment operator
    function that would seem to solve the problem of not freeing
    potentially usable memory before allocating the new array.

    A problem with that approach seems to be that it violates the
    guarantee of class invariance. Is this a correct observation on my
    part? Is there a way to accomplish both the goal of freeing available
    memory prior to allocating more, and preserving the class invariant?
    I can think of some approaches, but they all seem to add overhead
    to the assignment operator function.

    */


    --
    STH
    Hatton's Law: "There is only One inviolable Law"
    KDevelop: http://www.kdevelop.org SuSE: http://www.suse.com
    Mozilla: http://www.mozilla.org
  • Victor Bazarov

    #2
    Re: Exception safe at what cost

    Steven T. Hatton wrote:[color=blue]
    > I read Stroustrup's article of the day:
    > http://www.research.att.com/~bs/C++.html
    >
    > Programming with Exceptions. InformIt.com. April 2001.
    > http://www.research.att.com/~bs/eh_brief.pdf
    >
    > Some of these ideas are finally beginning to sink in. I believe I looked at
    > the same article a while back and decided I wasn't quite ready for it. If I
    > understood things correctly, there seems to be a slight problem with the
    > design of his exception safe assignment operator. I believe I have
    > correctly extracted all the relevant code, and appropriately commented it
    > to describe the various issues discussed in the article. My questions are
    > in the final comment block.
    >
    > class Vector { // v points to an array of sz ints
    > int sz;
    > int* v;
    > public: explicit Vector(int n); // create vector of n ints
    > Vector(const Vector&);
    > ~Vector(); // destroy vector
    > Vector& operator=(const Vector&); // assignment
    > int size() const;
    > void resize(int n); // change the size to n
    > int& operator[](int i); // subscripting
    > const int& operator[](int i) const; // subscripting
    > };
    >
    > Vector::Vector( int i) //constructor
    > :sz(i) ,v(new int[i]) { } // establishes class invariant
    >
    > Vector::~Vector () { delete[] v; }
    >
    > int Vector::size() const { return sz; } //no effect on class representation
    >
    > struct Bad_range { }; // I want an Über-exception to derive from!
    >
    > int& Vector::operato r[](int i)
    > {
    > if (0<=i && i<sz) return v[i];
    > throw Bad_range(); // no effect on class representation if thrown
    > }
    >
    >
    > /*
    > This is a bad assignment operator implementation because
    > it can lead to bad things like double deletion when exceptions
    > are thrown. That's because it fails to maintain the class invariant
    > that says a Vector holds an array
    >
    > */
    > Vector& Vector::operato r=(const Vector& a)
    > {
    > sz = a.sz; // get new size
    > delete[] v; // free old memory
    > v = new int[n]; // get new memory[/color]

    What 'n'? Don't you mean

    v = new int[a.sz];

    ???
    [color=blue]
    > copy(a.v,a.v+a. sz,v); // copy to new memory
    > }
    >
    > /*
    > The is a better version because it maintains the invariant
    > even if the memory allocation fails. We can probably trust
    > copy() not to throw an exception, or to otherwise fail,
    > so we don't worry about losing *p if it fails.
    > */
    > Vector& Vector::operato r=(const Vector& a)
    > {
    > int* p = new int[n]; // get new memory [or thorw bad_alloc?]
    > copy(a.v,a.v+a. sz,p); // copy to new memory
    >
    > /* invariant is violated in the next statement*/
    > sz = a.sz; // get new size
    > delete[] v; // free old memory
    >
    > /* invariant is reestablished in the next statement*/
    > v = p;
    > }
    >
    > /*
    > This is the example of where the problem with the first form
    > of Vector::operato r=(const Vector& a) might arise. Vector::v is
    > deleted once by the assignment operator, and then again when the
    > destructor Vector::~Vector () is called upon exit of the containing
    > block.
    > */
    > int main() {
    > try
    > {
    > Vector vec(10) ;
    > cout << vec.size() << ´\n´; // so far, so good
    > Vector v2(40*1000000) ; // ask for 160 megabytes
    > vec = v2; // use another 160 megabytes
    > }
    > catch(Range_err or) {
    > cerr << "Oops: Range error!\n";
    > }
    > catch(bad_alloc ) {
    > cerr << "Oops: memory exhausted!\n";
    > }
    > }
    >
    > /*
    > Now my observation is this:
    >
    > If I don't free the memory in Vector::v until the penultimate statement
    > in the assignment operator function, then I will not be able to
    > allocate as much memory as I would with the original (bad)
    > implementation.
    >
    > Earlier in the article Stroustrup provides this example of a
    > destructor: ~File_ptr() { if (p) fclose(p); }
    >
    > If I were to use the comparable ~Vector() { if(v) delete[] v; }[/color]

    Checking 'v' before deletion is useless. delete[] NULL does nothing.
    But if you don't clear 'v' after first deletion (in the operator=),
    then you are bound to try to delete it again here.
    [color=blue]
    > in conjunction with the first form of the assignment operator
    > function that would seem to solve the problem of not freeing
    > potentially usable memory before allocating the new array.[/color]

    No, it doesn't. Only if you fix the assignment operator to do this:

    Vector& Vector::operato r=(const Vector& a)
    {
    delete[] v; // free old memory
    v = 0; /// important: do not keep pointers that point to nothing
    sz = 0; // we got no storage -- indicate that in the size
    // at this point the state of the object is new, clean and consistent.
    // it's not as good as "allocate first, then assign" because you still
    // can lose data this way (if new allocation fails), but at least you
    // don't have any problems with double deallocation
    v = new int[a.sz]; // get new memory
    sz = a.sz; // successful allocation -- store new size
    copy(a.v,a.v+a. sz,v); // copy to new memory
    }

    Since the 'new int[a.sz]' can throw, 'v' will remain 0 in that case.
    [color=blue]
    >
    > A problem with that approach seems to be that it violates the
    > guarantee of class invariance. Is this a correct observation on my
    > part? Is there a way to accomplish both the goal of freeing available
    > memory prior to allocating more, and preserving the class invariant?
    > I can think of some approaches, but they all seem to add overhead
    > to the assignment operator function.[/color]

    Really?
    [color=blue]
    >
    > */[/color]

    Victor

    Comment

    • Steven T. Hatton

      #3
      Re: Exception safe at what cost

      Victor Bazarov wrote:
      [color=blue]
      > Steven T. Hatton wrote:[color=green]
      >> I read Stroustrup's article of the day:
      >> http://www.research.att.com/~bs/C++.html
      >>
      >> Programming with Exceptions. InformIt.com. April 2001.
      >> http://www.research.att.com/~bs/eh_brief.pdf[/color][/color]
      [...][color=blue][color=green]
      >> Vector& Vector::operato r=(const Vector& a)
      >> {
      >> sz = a.sz; // get new size
      >> delete[] v; // free old memory
      >> v = new int[n]; // get new memory[/color]
      >
      > What 'n'? Don't you mean
      >
      > v = new int[a.sz];
      >
      > ???[/color]
      :D

      There's actually another coding error in the article. There's no open
      parenthesis in main().
      [color=blue]
      > Checking 'v' before deletion is useless. delete[] NULL does nothing.
      > But if you don't clear 'v' after first deletion (in the operator=),
      > then you are bound to try to delete it again here.[/color]

      I wasn't sure what happened to 'v'. I should have looked it up before
      asking the question. § 5.3.5 informs me that the pointer is not nullified
      by a call to delete.
      [color=blue][color=green]
      >> in conjunction with the first form of the assignment operator
      >> function that would seem to solve the problem of not freeing
      >> potentially usable memory before allocating the new array.[/color]
      >
      > No, it doesn't. Only if you fix the assignment operator to do this:
      >
      > Vector& Vector::operato r=(const Vector& a)
      > {
      > delete[] v; // free old memory
      > v = 0; /// important: do not keep pointers that point to nothing
      > sz = 0; // we got no storage -- indicate that in the size
      > // at this point the state of the object is new, clean and consistent.
      > // it's not as good as "allocate first, then assign" because you still
      > // can lose data this way (if new allocation fails),[/color]

      I didn't think about that consequence of a failed alloc. Good point!
      [color=blue]
      > // but at least you
      > // don't have any problems with double deallocation
      > v = new int[a.sz]; // get new memory
      > sz = a.sz; // successful allocation -- store new size
      > copy(a.v,a.v+a. sz,v); // copy to new memory
      > }
      >
      > Since the 'new int[a.sz]' can throw, 'v' will remain 0 in that case.
      >[color=green]
      >>
      >> A problem with that approach seems to be that it violates the
      >> guarantee of class invariance. Is this a correct observation on my
      >> part? Is there a way to accomplish both the goal of freeing available
      >> memory prior to allocating more, and preserving the class invariant?
      >> I can think of some approaches, but they all seem to add overhead
      >> to the assignment operator function.[/color]
      >
      > Really?[/color]

      Well, it may not be (or seem like) a lot, but you did add two assignment
      statements to the function. I really don't know what the cost of that
      might be. If I am doing something like calculating the mutual
      gravitational attraction between 10,000 point masses during the generation
      of a display frame, those assignment operations might add up.
      --
      STH
      Hatton's Law: "There is only One inviolable Law"
      KDevelop: http://www.kdevelop.org SuSE: http://www.suse.com
      Mozilla: http://www.mozilla.org

      Comment

      • Alf P. Steinbach

        #4
        Re: Exception safe at what cost

        * Steven T. Hatton:[color=blue]
        >
        > Is there a way to accomplish both the goal of freeing available
        > memory prior to allocating more, and preserving the class invariant?[/color]

        You can preserve the class invariant but not the contents.

        --
        A: Because it messes up the order in which people normally read text.
        Q: Why is it such a bad thing?
        A: Top-posting.
        Q: What is the most annoying thing on usenet and in e-mail?

        Comment

        • Steven T. Hatton

          #5
          Re: Exception safe at what cost

          Victor Bazarov wrote:
          [color=blue]
          > Steven T. Hatton wrote:[color=green]
          >> I read Stroustrup's article of the day:
          >> http://www.research.att.com/~bs/C++.html
          >>
          >> Programming with Exceptions. InformIt.com. April 2001.
          >> http://www.research.att.com/~bs/eh_brief.pdf[/color][/color]
          [see previous reply for my comments on your observations.][color=blue]
          >
          > Victor[/color]

          BTW, it _may_ be possible to get real fancy with placement new() and legally
          recover the deallocated data upon bad_alloc. At what cost, I don't know.
          There's some discussion in §3.8 ¶7 of the Standard which suggests this may
          be possible.
          --
          STH
          Hatton's Law: "There is only One inviolable Law"
          KDevelop: http://www.kdevelop.org SuSE: http://www.suse.com
          Mozilla: http://www.mozilla.org

          Comment

          • Ali Cehreli

            #6
            Re: Exception safe at what cost

            On Thu, 05 Aug 2004 10:08:59 -0700, Steven T. Hatton wrote:
            [color=blue]
            > If I understood things correctly, there seems to be a slight problem
            > with the design of his exception safe assignment operator.[/color]

            [...]
            [color=blue]
            > /*
            > The is a better version because it maintains the invariant even if the
            > memory allocation fails. We can probably trust copy() not to throw an
            > exception, or to otherwise fail, so we don't worry about losing *p if
            > it fails.
            > */
            > Vector& Vector::operato r=(const Vector& a) {
            > int* p = new int[n]; // get new memory [or thorw bad_alloc?]
            > copy(a.v,a.v+a. sz,p); // copy to new memory
            >
            > /* invariant is violated in the next statement*/ sz = a.sz; // get new
            > size delete[] v; // free old memory
            >
            > /* invariant is reestablished in the next statement*/ v = p;
            > }[/color]

            This is the application of the principle of doing things that could
            potentially throw, on the side, before touching the state of the
            object; and then changing the state by using non-throwing statements.

            The equivalent of the above is first copying and then swapping, which
            is more idiomatically done by separate functions that do the same:

            // does not throw
            void Vector::swap(Ve ctor & other)
            {
            std::swap(sz, other.sz);
            std::swap(v, other.v);
            }

            Vector::Vector( Vector const & other)
            :
            sz(other.sz),
            v(new int[other.sz])
            {}

            Now the assignment operator is very easy and simple:

            Vector & Vector::operato r= (Vector const & other)
            {
            Vector tmp(other);
            this->swap(tmp);
            return *this;
            }

            Herb Sutter covers exception safety issues in his Exceptional C++
            book. Highly recommended...
            [color=blue]
            > If I don't free the memory in Vector::v until the penultimate
            > statement in the assignment operator function, then I will not be able
            > to allocate as much memory as I would with the original (bad)
            > implementation.[/color]

            Is it because the system memory is so limited that there is no room
            for another object? In that case we are dealing with a very special
            situation where the strong exception safety may not be provided. Then
            the version of the assignment operator with Victor Bazarov's
            modifications is suitable.

            Ali

            Comment

            • Ali Cehreli

              #7
              Re: Exception safe at what cost

              On Thu, 05 Aug 2004 12:02:28 -0700, Ali Cehreli wrote:
              [color=blue]
              > Vector::Vector( Vector const & other)
              > :
              > sz(other.sz),
              > v(new int[other.sz])
              > {}[/color]

              Oops! The body is missing the actual copy operation :( Should be:

              {
              std::copy(/* ... */);
              }

              Ali

              Comment

              • Victor Bazarov

                #8
                Re: Exception safe at what cost

                Ali Cehreli wrote:[color=blue]
                > On Thu, 05 Aug 2004 10:08:59 -0700, Steven T. Hatton wrote:
                >
                >[color=green]
                >>If I understood things correctly, there seems to be a slight problem
                >>with the design of his exception safe assignment operator.[/color]
                >
                >
                > [...]
                >
                >[color=green]
                >>/*
                >> The is a better version because it maintains the invariant even if the
                >> memory allocation fails. We can probably trust copy() not to throw an
                >> exception, or to otherwise fail, so we don't worry about losing *p if
                >> it fails.
                >> */
                >>Vector& Vector::operato r=(const Vector& a) {
                >> int* p = new int[n]; // get new memory [or thorw bad_alloc?]
                >> copy(a.v,a.v+a. sz,p); // copy to new memory
                >>
                >> /* invariant is violated in the next statement*/ sz = a.sz; // get new
                >> size delete[] v; // free old memory
                >>
                >> /* invariant is reestablished in the next statement*/ v = p;
                >>}[/color]
                >
                >
                > This is the application of the principle of doing things that could
                > potentially throw, on the side, before touching the state of the
                > object; and then changing the state by using non-throwing statements.
                >
                > The equivalent of the above is first copying and then swapping, which
                > is more idiomatically done by separate functions that do the same:
                >
                > // does not throw
                > void Vector::swap(Ve ctor & other)
                > {
                > std::swap(sz, other.sz);
                > std::swap(v, other.v);
                > }
                >
                > Vector::Vector( Vector const & other)
                > :
                > sz(other.sz),
                > v(new int[other.sz])
                > {}
                >
                > Now the assignment operator is very easy and simple:
                >
                > Vector & Vector::operato r= (Vector const & other)
                > {
                > Vector tmp(other);
                > this->swap(tmp);
                > return *this;[/color]

                You could just write

                Vector().swap(* this);
                return *this;
                [color=blue]
                > }[/color]

                And if you make 'swap' return its argument, you could even write

                return Vector().swap(* this);

                !
                [color=blue]
                >
                > Herb Sutter covers exception safety issues in his Exceptional C++
                > book. Highly recommended...
                >
                >[color=green]
                >> If I don't free the memory in Vector::v until the penultimate
                >> statement in the assignment operator function, then I will not be able
                >> to allocate as much memory as I would with the original (bad)
                >> implementation.[/color]
                >
                >
                > Is it because the system memory is so limited that there is no room
                > for another object? In that case we are dealing with a very special
                > situation where the strong exception safety may not be provided. Then
                > the version of the assignment operator with Victor Bazarov's
                > modifications is suitable.[/color]

                I thought that the amount of the available memory is basically the base
                for all the attempts to improve the implementation presented by Bjarne
                in his article... Anyway, good points and recommendations !

                Victor

                Comment

                • David Hilsee

                  #9
                  Re: Exception safe at what cost

                  "Steven T. Hatton" <susudata@setid ava.kushan.aa> wrote in message
                  news:mbednWRJ0d FU9I_cRVn-pw@speakeasy.ne t...
                  [snip][color=blue]
                  > If I don't free the memory in Vector::v until the penultimate statement
                  > in the assignment operator function, then I will not be able to
                  > allocate as much memory as I would with the original (bad)
                  > implementation.[/color]

                  If you're really worried about this, it might be good to switch to something
                  similar to std::deque, which allocates multiple blocks instead of one single
                  block. There's a little more overhead, but it's still pretty darn good.

                  --
                  David Hilsee


                  Comment

                  • Daniel T.

                    #10
                    Re: Exception safe at what cost

                    "Steven T. Hatton" <susudata@setid ava.kushan.aa> wrote:
                    [color=blue]
                    > class Vector { // v points to an array of sz ints
                    > int sz;
                    > int* v;
                    > public: explicit Vector(int n); // create vector of n ints
                    > Vector(const Vector&);
                    > ~Vector(); // destroy vector
                    > Vector& operator=(const Vector&); // assignment
                    > int size() const;
                    > void resize(int n); // change the size to n
                    > int& operator[](int i); // subscripting
                    > const int& operator[](int i) const; // subscripting
                    > };[/color]

                    void Vector::clear() {
                    // I would make this an inline function
                    delete [] v;
                    v = 0;
                    sz = 0;
                    }

                    Vector& Vector::operato r=(const Vector& other) {
                    if ( sz < other.sz ) {
                    clear();
                    v = new int[other.sz];
                    }
                    copy( other.v, other.v + other.sz, v );
                    sz = other.sz;
                    }

                    This solves the problems you had with the Vector class but introduces a
                    few wrinkles:
                    1) If new throws in the op=, the data that was in the vector may be lost.
                    2) The invariant has changed, now it's more like: if v != NULL then its
                    valid to dereference v[sz-1].

                    We could extend this concept and include a variable 'cap', which makes
                    it just that much more like std::vector.

                    Comment

                    • Ryan Mitchley

                      #11
                      Re: Exception safe at what cost

                      Some days I wish I were a Smalltalk programmer . . . or Prolog (my first
                      love, I guess).

                      Ryan


                      Comment

                      Working...