1 Meztilkis

Default Copy Constructor Assignment Operator Errors

The assignment operator (operator=) is used to copy values from one object to another already existing object.

Assignment vs Copy constructor

The purpose of the copy constructor and the assignment operator are almost equivalent -- both copy one object to another. However, the copy constructor initializes new objects, whereas the assignment operator replaces the contents of existing objects.

The difference between the copy constructor and the assignment operator causes a lot of confusion for new programmers, but it’s really not all that difficult. Summarizing:

  • If a new object has to be created before the copying can occur, the copy constructor is used (note: this includes passing or returning objects by value).
  • If a new object does not have to be created before the copying can occur, the assignment operator is used.

Overloading the assignment operator

Overloading the assignment operator (operator=) is fairly straightforward, with one specific caveat that we’ll get to. The assignment operator must be overloaded as a member function.

This prints:

5/3

This should all be pretty straightforward by now. Our overloaded operator= returns *this, so that we can chain multiple assignments together:

Issues due to self-assignment

Here’s where things start to get a little more interesting. C++ allows self-assignment:

This will call f1.operator=(f1), and under the simplistic implementation above, all of the members will be assigned to themselves. In this particular example, the self-assignment causes each member to be assigned to itself, which has no overall impact, other than wasting time. In most cases, a self-assignment doesn’t need to do anything at all!

However, in cases where an assignment operator needs to dynamically assign memory, self-assignment can actually be dangerous:

First, run the program as it is. You’ll see that the program prints “Alex” as it should.

Now run the following program:

You’ll probably get garbage output (or a crash). What happened?

Consider what happens in the overloaded operator= when the implicit object AND the passed in parameter (str) are both variable alex. In this case, m_data is the same as str._m_data. The first thing that happens is that the function checks to see if the implicit object already has a string. If so, it needs to delete it, so we don’t end up with a memory leak. In this case, m_data is allocated, so the function deletes m_data. But str.m_data is pointing to the same address! This means that str.m_data is now a dangling pointer.

Later on, when we’re copying the data from str into our implicit object, we’re accessing dangling pointer str.m_data. That leaves us either copying garbage data or trying to access memory that our application no longer owns (crash).

Detecting and handling self-assignment

Fortunately, we can detect when self-assignment occurs. Here’s a better implementation of our overloaded operator= for the Fraction class:

By checking if our implicit object is the same as the one being passed in as a parameter, we can have our assignment operator just return immediately without doing any other work.

Note that there is no need to check for self-assignment in a copy-constructor. This is because the copy constructor is only called when new objects are being constructed, and there is no way to assign a newly created object to itself in a way that calls to copy constructor.

Default assignment operator

Unlike other operators, the compiler will provide a default public assignment operator for your class if you do not provide one. This assignment operator does memberwise assignment (which is essentially the same as the memberwise initialization that default copy constructors do).

Just like other constructors and operators, you can prevent assignments from being made by making your assignment operator private or using the delete keyword:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

#include <cassert>

#include <iostream>

 

classFraction

{

private:

intm_numerator;

intm_denominator;

 

public:

    // Default constructor

    Fraction(intnumerator=0,intdenominator=1):

        m_numerator(numerator),m_denominator(denominator)

    {

        assert(denominator!=0);

    }

 

// Copy constructor

Fraction(constFraction&copy):

m_numerator(copy.m_numerator),m_denominator(copy.m_denominator)

{

// no need to check for a denominator of 0 here since copy must already be a valid Fraction

std::cout<<"Copy constructor called\n";// just to prove it works

}

 

        // Overloaded assignment

        Fraction&operator=(constFraction&fraction);

 

friendstd::ostream&operator<<(std::ostream&out,constFraction&f1);

        

};

 

std::ostream&operator<<(std::ostream&out,constFraction&f1)

{

out<<f1.m_numerator<<"/"<<f1.m_denominator;

returnout;

}

 

// A simplistic implementation of operator= (see better implementation below)

Fraction&Fraction::operator=(constFraction&fraction)

{

    // do the copy

    m_numerator=fraction.m_numerator;

    m_denominator=fraction.m_denominator;

 

    // return the existing object so we can chain this operator

    return*this;

}

 

intmain()

{

    Fraction fiveThirds(5,3);

    Fractionf;

    f=fiveThirds;// calls overloaded assignment

    std::cout<<f;

 

    return0;

}

intmain()

{

    Fraction f1(5,3);

    Fraction f2(7,2);

    Fraction f3(9,5);

 

    f1=f2=f3;// chained assignment

 

    return0;

}

intmain()

{

    Fraction f1(5,3);

    f1=f1;// self assignment

 

    return0;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

#include <iostream>

 

classMyString

{

private:

    char*m_data;

    intm_length;

 

public:

    MyString(constchar*data="",intlength=0):

        m_length(length)

    {

        if(!length)

            m_data=nullptr;

        else

            m_data=newchar[length];

 

        for(inti=0;i<length;++i)

            m_data[i]=data[i];

    }

 

    // Overloaded assignment

    MyString&operator=(constMyString&str);

 

    friendstd::ostream&operator<<(std::ostream&out,constMyString&s);

};

 

std::ostream&operator<<(std::ostream&out,constMyString&s)

{

    out<<s.m_data;

    returnout;

}

 

// A simplistic implementation of operator= (do not use)

MyString&MyString::operator=(constMyString&str)

{

    // if data exists in the current string, delete it

    if(m_data)delete[]m_data;

 

    m_length=str.m_length;

 

    // copy the data from str to the implicit object

    m_data=newchar[str.m_length];

 

    for(inti=0;i<str.m_length;++i)

        m_data[i]=str.m_data[i];

 

    // return the existing object so we can chain this operator

    return*this;

}

 

intmain()

{

    MyString alex("Alex",5);// Meet Alex

    MyString employee;

    employee=alex;// Alex is our newest employee

    std::cout<<employee;// Say your name, employee

 

    return0;

}

intmain()

{

    MyString alex("Alex",5);// Meet Alex

    alex=alex;// Alex is himself

    std::cout<<alex;// Say your name, Alex

 

    return0;

}

// A better implementation of operator=

Fraction&Fraction::operator=(constFraction&fraction)

{

    // self-assignment guard

    if(this==&fraction)

        return*this;

 

    // do the copy

    m_numerator=fraction.m_numerator;

    m_denominator=fraction.m_denominator;

 

    // return the existing object so we can chain this operator

    return*this;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

#include <cassert>

#include <iostream>

 

classFraction

{

private:

intm_numerator;

intm_denominator;

 

public:

    // Default constructor

    Fraction(intnumerator=0,intdenominator=1):

        m_numerator(numerator),m_denominator(denominator)

    {

        assert(denominator!=0);

    }

 

// Copy constructor

Fraction(constFraction&copy)=delete;

 

// Overloaded assignment

Fraction&operator=(constFraction&fraction)=delete;// no copies through assignment!

 

friendstd::ostream&operator<<(std::ostream&out,constFraction&f1);

        

};

 

std::ostream&operator<<(std::ostream&out,constFraction&f1)

{

out<<f1.m_numerator<<"/"<<f1.m_denominator;

returnout;

}

 

intmain()

{

    Fraction fiveThirds(5,3);

    Fractionf;

    f=fiveThirds;// compile error, operator= has been deleted

    std::cout<<f;

 

    return0;

}

Copy Constructor / Assignment Operators

In C++ you can construct one instance from another via a constructor and also by an assignment operator. In some cases a constructor will be used instead of an assignment:

By default C++ generates all the code to copy and assign the bytes in one class to another without any effort. Lucky us!

So our class PersonList might look like this:

Except we're not lucky, we just got slimed. The default byte copy takes the pointer in and makes a copy of it. Now if we copy to , or assign to we have three classes pointing to the same private data! On top of that, allocated its own during its default constructor but the byte copy assignment overwrote it with the one from so its old value just leaks.

Of course we might be able to use a to hold our pointer. In which case the compiler would generate an error. But it might not always be that simple. may have been opaquely allocated by an external library so have no choice but to manage its lifetime through the constructor and destructor.

The Rule of Three

This is such a terrible bug enabling problem in C++ that it has given rise to the so-called the Rule of Three1.

The rule says that if we explicitly declare a destructor, copy constructor or copy assignment operator in a C++ class then we probably need to implement all three of them to safely handle assignment and construction. In other words the burden for fixing C++'s default and dangerous behaviour falls onto the developer.

So let's fix the class:

What a mess!

We've added a copy constructor and an assignment operator to the class to handle copying safely. The code even had to check if it was being assigned to itself in case someone wrote . Without that test, the receiving instance would clear itself in preparation to adding elements from itself which would of course wipe out all its contents.

Alternatively we might disable copy / assignments by creating private constructors that prevents them being called by external code:

Another alternative would be to use noncopyable types within the class itself. For example, the copy would fail if the pointer were managed with a C++11 (or Boost's ).

Boost also provides a class which provides yet another option. Classes may inherit from noncopyable which implements a private copy constructor and assignment operator so any code that tries to copy will generate a compile error.

The Rule of Five

The Rule of Three has become the Rule of Five(!) in C++11 because of the introduction of move semantics.

If you have a class that can benefit from move semantics, the Rule of Five essentially says that the existence of the user-defined destructor, copy constructor and copy assignment operator requires you to also implement a move constructor and a move assignment operator. So in addition to the code we wrote above we must also write two more methods.

How Rust helps

Move is the default

Rust helps by making move semantics the default. i.e. unless you need to copy data from one instance to another, you don't. If you assign a struct from one variable to another, ownership moves with it. The old variable is marked invalid by the compiler and it is an error to access it.

But if you do want to copy data from one instance to another then you have two choices.

  • Implement the trait. Your struct will have an explicit function you can call to make a copy of the data.
  • Implement the trait. Your struct will now implicitly copy on assignment instead of move. Implementing also implies implementing so you can still explicitly call if you prefer.

Primitive types such as integers, chars, bools etc. implement so you can just assign one to another

But a cannot be copied this way. A string has an internal heap allocated pointer so copying is a more expensive operation. So only implements the trait which requires you to explicitly duplicate it:

The default for any struct is that it can neither be copied nor cloned.

The following code will create a object, assigns it to . And when is assigned to , ownership of the data also moves:

Attempting to use after ownership moves to will generate a compile error:

To illustrate consider this Rust which is equivalent to the PersonList we saw in C++

We can see that has a vector of objects. Under the covers the will allocate space in the heap to store its data.

Now let's use it.

The variable is on the stack and is a but the persons member is partly allocated from the heap.

The variable is bound to a PersonList on the stack. The vector is created in the heap. If we assign to then we could have two stack objects sharing the same pointer on the heap in the same way we did in C++.

But Rust stops that from happening. When we assign to , the compiler will do a bitwise copy of the data in x, but it will bind ownership to . When we try to access the in the old var Rust generates a compile error.

Rust has stopped the problem that we saw in C++. Not only stopped it but told us why it stopped it - the value moved from x to y and so we can't use x any more.

Implementing the Copy trait

The trait allows us to do direct assignment between variables. The trait has no functions, and acts as a marker in the code to denote data that should be duplicated on assignment.

You can implement the trait by deriving it, or implementing it. But you can only do so if all the members of the struct also derive the trait:

So is copyable because types and are also copyable and the compiler will take the directive and modify the move / copy semantics for the struct.

But when a struct contains a a type that does not implement you will get a compiler error. So this struct will cause a compiler error because does not implement

Implementing the Clone trait

The trait adds a function to your struct that produces an independent copy of it. We can derive it if every member of the struct can be cloned which in the case of it can:

Now that Person derives , we can do the same for PersonList because all its member types implement that trait - a Person can be cloned, a Vec can be cloned, and a Box can be cloned:

And now we can clone into and we have two independent copies.

Summary

In summary, Rust stops us from getting into trouble by treated assigns as moves when a non-copyable variable is assigned from one to another. But if we want to be able to clone / copy we can make our intent explicit and do that too.

C++ just lets us dig a hole and fills the dirt in on top of us.

Leave a Comment

(0 Comments)

Your email address will not be published. Required fields are marked *