Question

I had a conversation with a friend of mine about object assignment and construction the other day, and he made a point that assignment a = b for objects is (semantically) equivalent to destroying a and then re-constructing it from b (at the same place).

But of course, nobody (I think) writes assignment operators like this:

class A {
    A& operator=(const A& rhs) {
        this->~A();
        this->A(rhs);
        return *this;
    }

    A& operator=(A&& rhs) {
        this->~A();
        this->A(std::move(rhs));
        return *this;
    }

    // etc.
};

[Notice: I have no clue how to manually call constructors/destructors on existing objects (I never had to do that!), so their invocations may make no formal sense, but I guess you can see the idea.]

What are the problems with this approach? I imagine there has to be a main show-stopper, but the bigger the list, the better.

Was it helpful?

Solution 2

First of all, calling the destructor manually is required only if the object was constructed using an overloaded operator new() with some expections like using the std::nothrow overloads.

And what you got to understand is the difference between copy construction and assignment operator: copy constructor is called when a new object is created from an existing object, as a copy of the existing object. And assignment operator is called when an already initialized object is assigned a new value from another existing object.

To sum up, example of assignment operator you've provided doesn't make sense - it got to have different semantics.

If you have further questions, leave a comment.

OTHER TIPS

There is a misused contruction, here:

class A {
    A& operator=(const A& rhs) {
        if(&a==this) return *this; 
        this->~A();
        new(this) A(rhs);
        return *this;
    }

    A& operator=(A&& rhs) {
        if(&a==this) return *this; 
        this->~A();
        new(this) A(std::move(rhs));
        return *this;
    }

    // etc.
};

This is correct respect to the inplace ctor/dtor semantics, and thsi is what std::allocator does to destroy and construct elements in a buffer, so that must be correct, right?

Well... not properly: it all is about what A in fact contains and what the A ctor actually does.

If A just contains basic types and does not own resources that's fine, it works. It's just not idiomatic, but correct.

If A contains some other resources, that need to be acquired, managed and released well... you may be in trouble. And you also are if A is polymorphic (if ~A is virtual you destroy the entire object, but then you reconstruct just the A subobject).

The problem is that a constructor that acquires resources may fail, and an object that fails in construction and throws must not be destroyed since it has been never "constructed".

But if you are "assigning", you are not "creating", and if the in-place ctor fails, your object will exist (because it pre-exist in its own scope), but is in a state that cannot be managed by a further destruction: think to

{
  A a,b;
  a = b;
}

At the } b and a will be destroyed but if A(const A&) failed in a=b, and a throw is made in A::A, a is not existing, but will be improperly destroyed at the } that throw will immediately jump to.

A more idiomatic way is to have

class A
{
   void swap(A& s) noexcept
   { /* exchanging resources between existing objects should never fail: you just swap pointers */ }
public:
   A() noexcept { /* creates an object in a "null" recognizable state */ }
   A(const A& s) { /* creates a copy: may fail! */ }
   A(A&& s) noexcept { /*make it as null and... */ swap(s); } // if `s` is temporary will caryy old resource deletionon, and we keep it's own resource going
   A& operator=(A s) noexcept { swap(s); return *this; }
   ~A() { /* handle resource deletion, if any */ }
};

Now,

  a=b

will create a b copy as the s parameter in operator= (by means of A::A(const A&)). If this fails, s will not exist and a and b are still valid (with their own old values), hence at scope exiting will be destroyed as normally. If the copy succeed, the copyed resources and the actual a's will be exchanged, and when s dies at the } the old-a resources will be freed.

By converse

a = std::move(b)

Will make b as-temporary, the s parameter constructed via A(A&&), so b will swap with s (and becomes null) than s will swap with a. At the end, s will destroy old a resources, a will receive old b's and b will be in null state (so it can die peacefully when its scope ends)

The problem of "making A as null" must be implemented in both A() and A(A&&). This may be by means of an helper member (an init, just like a swap) or by specifying member initializers, or by defining default initialization values for members (once for all)

First it is not legal to call a copy constructor directly (at least in C++ compliant compilers.. VS2012 allows that) so the following isn't allowed:

  // assignment operator
  A& operator=(const A& rhs) {
  this->~A();
  this->A::A(rhs); <---  Invalid use

at that point you can either rely on compiler optimizations (see copy elision and RVO) or allocate it on the heap.

Many issues can arise if you try to do the above:

1) You might have exceptions thrown in the expression for the copy constructor

In this case you will have

  // assignment operator
  A& operator=(const A& rhs) {
    cout << "copy assignment called" << endl;
    this->~A();
    A newObj(rhs); // Can throw and A is in invalid state!
    return newObj;
  }

To make it safe you should use the copy-and-swap idiom:

set& set::operator=(set const& source)
{
    /* You actually don't need this. But if creating a copy is expensive then feel free */
    if (&source == this)
        return;

    /*
     * This line is invoking the copy constructor.
     * You are copying 'source' into a temporary object not the current one.
     * But the use of the swap() immediately after the copy makes it logically
     * equivalent.
     */
    set tmp(source);
    this->swap(tmp);

    return *this;
}

void swap(set& dst) throw ()
{
    // swap member of this with members of dst
}

2) You might have problems with dynamically allocated memory

In case two instances of A shared a pointer, you might have a dangling pointer before being able to release it

  a = a; // easiest case

  ...

  // assignment operator
  A& operator=(const A& rhs) {
  this->~A(); <-- Freeing dynamically allocated memory
  this->A::A(rhs); <---  Getting a pointer to nowhere

3) As Emilio noted, if the class is polymorphic you're not going to be able to re-instantiate that subclass (unless you trick it somehow with a CRTP-like technique)

4) Finally assignment and copy construction are two different operations. If A contains resources which are expensive to re-acquire, you might find yourself in a lot of troubles.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top