Overloading Operators C++ Assignments

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;

}

A copy assignment operator of class is a non-template non-static member function with the name operator= that takes exactly one parameter of type T, T&, const T&, volatile T&, or constvolatile T&. For a type to be , it must have a public copy assignment operator.

[edit]Syntax

class_nameclass_name ( class_name ) (1)
class_nameclass_name ( const class_name ) (2)
class_nameclass_name ( const class_name ) = default; (3) (since C++11)
class_nameclass_name ( const class_name ) = delete; (4) (since C++11)

[edit]Explanation

  1. Typical declaration of a copy assignment operator when copy-and-swap idiom can be used.
  2. Typical declaration of a copy assignment operator when copy-and-swap idiom cannot be used (non-swappable type or degraded performance).
  3. Forcing a copy assignment operator to be generated by the compiler.
  4. Avoiding implicit copy assignment.

The copy assignment operator is called whenever selected by overload resolution, e.g. when an object appears on the left side of an assignment expression.

[edit]Implicitly-declared copy assignment operator

If no user-defined copy assignment operators are provided for a class type (struct, class, or union), the compiler will always declare one as an inline public member of the class. This implicitly-declared copy assignment operator has the form T& T::operator=(const T&) if all of the following is true:

  • each direct base of has a copy assignment operator whose parameters are B or const B& or constvolatile B&;
  • each non-static data member of of class type or array of class type has a copy assignment operator whose parameters are M or const M& or constvolatile M&.

Otherwise the implicitly-declared copy assignment operator is declared as T& T::operator=(T&). (Note that due to these rules, the implicitly-declared copy assignment operator cannot bind to a volatile lvalue argument.)

A class can have multiple copy assignment operators, e.g. both T& T::operator=(const T&) and T& T::operator=(T). If some user-defined copy assignment operators are present, the user may still force the generation of the implicitly declared copy assignment operator with the keyword .(since C++11)

The implicitly-declared (or defaulted on its first declaration) copy assignment operator has an exception specification as described in dynamic exception specification(until C++17)exception specification(since C++17)

Because the copy assignment operator is always declared for any class, the base class assignment operator is always hidden. If a using-declaration is used to bring in the assignment operator from the base class, and its argument type could be the same as the argument type of the implicit assignment operator of the derived class, the using-declaration is also hidden by the implicit declaration.

[edit]Deleted implicitly-declared copy assignment operator

A implicitly-declared copy assignment operator for class is defined as deleted if any of the following is true:

  • has a user-declared move constructor;
  • has a user-declared move assignment operator.

Otherwise, it is defined as defaulted.

A defaulted copy assignment operator for class is defined as deleted if any of the following is true:

  • has a non-static data member of non-class type (or array thereof) that is const;
  • has a non-static data member of a reference type;
  • has a non-static data member or a direct or virtual base class that cannot be copy-assigned (overload resolution for the copy assignment fails, or selects a deleted or inaccessible function);
  • is a union-like class, and has a variant member whose corresponding assignment operator is non-trivial.

[edit]Trivial copy assignment operator

The copy assignment operator for class is trivial if all of the following is true:

  • it is not user-provided (meaning, it is implicitly-defined or defaulted) , , and if it is defaulted, its signature is the same as implicitly-defined(until C++14);
  • has no virtual member functions;
  • has no virtual base classes;
  • the copy assignment operator selected for every direct base of is trivial;
  • the copy assignment operator selected for every non-static class type (or array of class type) member of is trivial;
  • has no non-static data members of volatile-qualified type.
(since C++14)

A trivial copy assignment operator makes a copy of the object representation as if by std::memmove. All data types compatible with the C language (POD types) are trivially copy-assignable.

[edit]Implicitly-defined copy assignment operator

If the implicitly-declared copy assignment operator is neither deleted nor trivial, it is defined (that is, a function body is generated and compiled) by the compiler if odr-used. For union types, the implicitly-defined copy assignment copies the object representation (as by std::memmove). For non-union class types (class and struct), the operator performs member-wise copy assignment of the object's bases and non-static members, in their initialization order, using built-in assignment for the scalars and copy assignment operator for class types.

The generation of the implicitly-defined copy assignment operator is deprecated(since C++11) if has a user-declared destructor or user-declared copy constructor.

[edit]Notes

If both copy and move assignment operators are provided, overload resolution selects the move assignment if the argument is an rvalue (either a prvalue such as a nameless temporary or an xvalue such as the result of std::move), and selects the copy assignment if the argument is an lvalue (named object or a function/operator returning lvalue reference). If only the copy assignment is provided, all argument categories select it (as long as it takes its argument by value or as reference to const, since rvalues can bind to const references), which makes copy assignment the fallback for move assignment, when move is unavailable.

It is unspecified whether virtual base class subobjects that are accessible through more than one path in the inheritance lattice, are assigned more than once by the implicitly-defined copy assignment operator (same applies to move assignment).

See assignment operator overloading for additional detail on the expected behavior of a user-defined copy-assignment operator.

[edit]Example

Run this code

Output:

#include <iostream>#include <memory>#include <string>#include <algorithm>   struct A {int n;std::string s1;// user-defined copy assignment, copy-and-swap form A& operator=(A other){std::cout<<"copy assignment of A\n";std::swap(n, other.n);std::swap(s1, other.s1);return*this;}};   struct B : A {std::string s2;// implicitly-defined copy assignment};   struct C {std::unique_ptr<int[]> data;std::size_t size;// non-copy-and-swap assignment C& operator=(const C& other){// check for self-assignmentif(&other == this)return*this;// reuse storage when possibleif(size != other.size){ data.reset(new int[other.size]); size = other.size;}std::copy(&other.data[0], &other.data[0]+ size, &data[0]);return*this;}// note: copy-and-swap would always cause a reallocation};   int main(){ A a1, a2;std::cout<<"a1 = a2 calls "; a1 = a2;// user-defined copy assignment   B b1, b2; b2.s1="foo"; b2.s2="bar";std::cout<<"b1 = b2 calls "; b1 = b2;// implicitly-defined copy assignmentstd::cout<<"b1.s1 = "<< b1.s1<<" b1.s2 = "<< b1.s2<<'\n';}
a1 = a2 calls copy assignment of A b1 = b2 calls copy assignment of A b1.s1 = foo b1.s2 = bar

[edit]Defect reports

The following behavior-changing defect reports were applied retroactively to previously published C++ standards.

DR Applied to Behavior as published Correct behavior
CWG 2171 C++14 operator=(X&)=default was non-trivial made trivial

One thought on “Overloading Operators C++ Assignments

Leave a Reply

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