c++exceptionassignment-operatorexception-safety

What is wrong with "checking for self-assignment" and what does it mean?


In Herb Sutter's book Exceptional C++ (1999), he has words in item 10's solution:

"Exception-unsafe" and "poor design" go hand in hand. If a piece of code isn't exception-safe, that's generally okay and can simply be fixed. But if a piece of code cannot be made exception-safe because of its underlying design, that almost always is a signal of its poor design.

Example 1: A function with two different responsibilities is difficult to make exception-safe.

Example 2: A copy assignment operator that is written in such a way that it must check for self-assignment is probably not strongly exception-safe either

What does he mean by the term "check for self-assignment"?

[INQUIRY]

Dave and AndreyT shows us exactly what "check for self-assignment" means. That's good. But the question is not over. Why does "check for self-assignment" hurts "exception safety"(according to Hurb Sutter)? If the caller tries to do self-assignment, that "check" works as if no assignment ever occurs. Does it really hurt?

[MEMO 1] In item 38 Object Identity later in Herb's book, he explains about self-assignment.


Solution

  • A question that's of greater importance in this case would be:



    To answer my rhetorical question: It means that a well-designed assignment operator should not need to check for self-assignment. Assigning an object to itself should work correctly (i.e. have the end-effect of "doing nothing") without performing an explicit check for self-assignment.

    For example, if I wanted to implement a simplistic array class along the lines of

    class array {
        // code...
     
        int *data;
        size_t n;
    };
    

    ...and came up with the following implementation of the assignment operator...

    array &array::operator =(const array &rhs) 
    {
        delete[] data;
    
        n = rhs.n;
        data = new int[n];
    
        std::copy_n(rhs.data, n, data);
    
        return *this;
    }
    
    That implementation would be considered "bad" since it obviously fails in case of self-assignment.

    In order to "fix" this, you have two options;

    1. Add an explicit self-assignment check
    array &array::operator =(const array &rhs) {
        if (&rhs != this) {
            delete[] data;
    
            n = rhs.n;
            data = new int[n];
    
            std::copy_n(rhs.data, n, data);
        }
        return *this;
    }
    
    1. Follow a "check-less" approach:
    array &array::operator =(const array &rhs) {
          size_t new_n = rhs.n;
          int *new_data = new int[new_n];
    
          std::copy_n(rhs.data, new_n, new_data);
    
          delete[] data;
    
          n = new_n;
          data = new_data;
    
          return *this;
    }
    

    The latter approach is better in a sense that it works correctly in self-assignment situations without making an explicit check. This implementation is far for perfect from a 'safety point of view', it is here to illustrate the difference between "checked" and "check-less" approaches to handling self-assignment. The later check-less implementation can be written more elegantly through the well-known copy-and-swap idiom.

    This does not mean that you should avoid explicit checks for self-assignment. Such check do make sense from the performance point of view: there's no point in carrying out a long sequence of operations just to end up "doing nothing" in the end. But in a well-designed assignment operator such checks should not be necessary from the correctness point of view.