0

I am studying C++ at University, and in the break I am going through Strousrtup's "The CPP Programming Language 4th Edition" to fill in the gaps of my understanding and what we are being taught in class.

In section 3.3.1 he details a code snippet for a simplified version of a vector class (which is type-specific to doubles only):

Vector& Vector::operator=(const Vector& a) {
    double* p = new double[a.sz]; 
    for (int i=0; i!=a.sz; ++i)
        p[i] = a.elem[i]; 
    delete[] elem;
    elem = p;
    sz = a.sz;
    return *this;
}

Now, I had already written my own version of an overridden copy assignment operator to go along with this simplified quasi-vector before I saw this, which seems to work correctly, but I was wondering, is there anything wrong with deleting the memory allocated that elem points to and then re-initialising it, as I do below, compared to how Stroustrup does it?

vectoR& vectoR::operator=(const vectoR& v) {
    delete[] elem;
    elem = new double[v.size()];
    sz = v.size();
    for (int i = 0; i != sz; ++i) 
        elem[i] = v[i];
    return *this;
}
Benjamin R
  • 555
  • 6
  • 25
  • That design would make your vector very unintuitive to any other C++ programmer who tried to pick up your code – M.M Apr 15 '14 at 05:43
  • @MattMcNabb to kill this irrelevant line of thought I will make the condition of the for-loop consistent with Stroustrup's formatting. These comments are not pertinent to the question being asked. – Benjamin R Apr 15 '14 at 06:08
  • Thanks everyone for your help. I have so much to learn! – Benjamin R Apr 15 '14 at 21:24

3 Answers3

5

Yes, Strousrtup's way is self-assignment safe. That is, an instance can be assigned to itself

a = a;

Once you've finished that book you may want to scoot through Meyer's "Effective C++" (2005) which is also an excellent text and considers such problems as these.

Cramer
  • 1,785
  • 1
  • 12
  • 20
  • That's the straightforward answer I was looking for! I will mark it as answered once it lets me in ten minutes. Thank you! – Benjamin R Apr 15 '14 at 05:18
  • 4
    You might notice exception safety, too –  Apr 15 '14 at 05:19
  • +1 I love clear answers like this. And reading Stroustrup is an excellent idea. – Bathsheba Apr 15 '14 at 05:20
  • @DieterLücking can you expand upon your comment about exception safety in Stroustrup's code? Just to make sure I understand... – Benjamin R Apr 15 '14 at 05:27
  • @BenjaminR If an exception occurs during building the new data, the old data stays intact and will not be altered. –  Apr 15 '14 at 05:31
  • Given that `a = a;` is possible, wouldn't it be wise to make the function start with `if ( &a == this ) return *this;` to avoid wasting a lot of time? – M.M Apr 15 '14 at 05:42
  • If you look at the chapter in Stroustrup's book on exceptions, he uses vector as an example and goes through it in more detail on efficiency and exception safety. – sj0h Apr 15 '14 at 05:48
  • @MattMcNabb That's true... but an even better solution can be found in the top answer to this question: http://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom – Benjamin R Apr 15 '14 at 06:15
  • @Matt That would be considered premature pessimization. Self-assignment is very rare so it would be unwise to optimize this case, while burdening the normal case. Self-assignment needs to be correct, but it doesn't have to be fast. – D Drmmr Apr 15 '14 at 06:41
  • This answer is incorrect. It's easy to protect against self assignment; the real problem here is that your implementation is incorrect even without self assignment. – James Kanze Apr 15 '14 at 08:56
  • @MattMcNabb You could, but... the function remains incorrect. In general, if you think you have to test for self-assignment, there's a good chance that your implementation is incorrect. – James Kanze Apr 15 '14 at 08:58
4

Stroustrup's implementation will not destroy the existing elem if an exception occurs, and allows self-assignment.

Woody
  • 145
  • 1
  • 4
2

Your version is incorrect, and has undefined behavior. What happens if new double[v.size()] throws an exception?

In general, you shouldn't do anything which can invalidate an object until after you've done everything which might throw and exception. Leaving a point to deleted memory results in an invalid object, that new can always throw, so you shouldn't delete elem until after you've done the new.

EDIT:

To be more explicit: from the original poster's suggested implementation:

delete[] elem;
elem = new double[v.size()];

The first line invalidates the pointer elem, and if there is an exception in the second line (and new can always throw an exception), then the assignment operator leaves the object with the invalid pointer; any further access to this pointer, including in the destructor of the object, is undefined behavior.

There are, in fact, many ways of avoiding this problem in this particular instance:

delete[] elem;
elem = nullptr;
elem = new double[v.size()];

for example (provided that any functions called on the object can deal with a null pointer), or (what is effectively the same thing):

delete[] elem;
elem = new (std::nothrow) double[v.size()];
if ( elem == nullptr )
    throw std::bad_alloc();

Both of these solutions are in many ways special, however, and not generally applicable. They also leave the object in a special state, which may require extra handling. The usual solution is to do anything that can throw before modifying any of the state of the object. In this case, the only thing which can throw is the new, and we end up with Stroustrup's solution. In more complicated objects, the necessary solution may be more complicated; one common simple solution is the swap idiom:

MyType& MyType::operator=( MyType const& other )
{
    MyType tmp( other );    //  Copy constructor
    swap( tmp );            //  Member function, guaranteed nothrow
    return *this;
}

This works well if you can write a nonthrow swap member function. You often can, because swapping pointers is a nothrow (so in this case, all swap would do is swap elem), but it is not a given. Each case needs to be evaluated individually.

The swap idiom does give the "strong" guarantee: either the assignment fully succeeds, or the object's state is unchanged. You don't often need this guarantee, however; it's usually sufficient that the object be in some coherent state (so that it can be destructed).

Finally: if your class has several resources, you'll almost certainly want to encapsulate them in some sort of RAII class (e.g. smart pointer) or in separate base classes, so that you can make the constructors exception safe, so that they won't leak the first resource if allocating the second fails. This can be a useful technique even in cases where there is only one resource; in the original example, if elem had been an std::unique_ptr<double[]>, no delete would have been necessary in the assignment operator, and just:

elem = new double[v.size()];
//  copy...

is all that would be needed. In practice, if real code, cases where this solves the solution are fairly rare; in real code, for example, the orginal problem would be solved with std::vector<double> (and the requirements of std::vector are such that std::unique_ptr is not really a solution). But they do exist, and classes like std::unique_ptr (or an even simpler scoped pointer) are certainly a solution worth having in your toolkit.

James Kanze
  • 150,581
  • 18
  • 184
  • 329
  • I would check this as an answer, except that it's not explicit enough for someone who is learning to know exactly what to do to correct/prevent the problems you describe. I think I understand what you mean, but could you provide some correct(ed) code to back it up? – Benjamin R Apr 15 '14 at 21:27
  • thanks, checked it as answered. I appreciate the extra detail, it's very helpful! – Benjamin R May 06 '14 at 02:59