Why does the = operator work on structs without having been defined?

conejoroy picture conejoroy · Oct 15, 2009 · Viewed 63.1k times · Source

Let's look at a simple example:

struct some_struct {
   std::string str;
   int a, b, c;
}

some_struct abc, abc_copy;
abc.str = "some text";
abc.a = 1;
abc.b = 2;
abc.c = 3;

abc_copy = abc;

Then abc_copy is an exact copy of abc.. how is it possible without defining the = operator?

(This took me by surprise when working on some code..)

Answer

Martin York picture Martin York · Oct 16, 2009

If you do not define these four methods (six in C++11) the compiler will generate them for you:

  • Default Constructor
  • Copy Constructor
  • Assignment Operator
  • Destructor
  • Move Constructor (C++11)
  • Move Assignment (C++11)

If you want to know why?
It is to maintain backward compatibility with C (because C structs are copyable using = and in declaration). But it also makes writing simple classes easier. Some would argue that it adds problems because of the "shallow copy problem". My argument against that is that you should not have a class with owned RAW pointers in it. By using the appropriate smart pointers that problem goes away.

Default Constructor (If no other constructors are defined)

The compiler generated default constructor will call the base classes default constructor and then each members default constructor (in the order they are declared)

Destructor (If no destructor defined)

Calls the destructor of each member in reverse order of declaration. Then calls the destructor of the base class.

Copy Constructor (If no copy constructor is defined)

Calls the base class copy constructor passing the src object. Then calls the copy constructor of each member using the src objects members as the value to be copied.

Assignment Operator

Calls the base class assignment operator passing the src object. Then calls the assignment operator on each member using the src object as the value to be copied.

Move Constructor (If no move constructor is defined)

Calls the base class move constructor passing the src object. Then calls the move constructor of each member using the src objects members as the value to be moved.

Move Assignment Operator

Calls the base class move assignment operator passing the src object. Then calls the move assignment operator on each member using the src object as the value to be copied.

If you define a class like this:

struct some_struct: public some_base
{   
    std::string str1;
    int a;
    float b;
    char* c;
    std::string str2;
};

What the compiler will build is:

struct some_struct: public some_base
{   
    std::string str1;
    int a;
    float b;
    char* c;
    std::string str2;

    // Conceptually two different versions of the default constructor are built
    // One is for value-initialization the other for zero-initialization
    // The one used depends on how the object is declared.
    //        some_struct* a = new some_struct;     // value-initialized
    //        some_struct* b = new some_struct();   // zero-initialized
    //        some_struct  c;                       // value-initialized
    //        some_struct  d = some_struct();       // zero-initialized
    // Note: Just because there are conceptually two constructors does not mean
    //       there are actually two built.

    // value-initialize version
    some_struct()
        : some_base()            // value-initialize base (if compiler generated)
        , str1()                 // has a normal constructor so just call it
        // PODS not initialized
        , str2()
   {}

    // zero-initialize version
    some_struct()
        : some_base()            // zero-initialize base (if compiler generated)
        , str1()                 // has a normal constructor so just call it.
        , a(0)
        , b(0)
        , c(0)   // 0 is NULL
        , str2()
        // Initialize all padding to zero
   {}

    some_struct(some_struct const& copy)
        : some_base(copy)
        , str1(copy.str1)
        , a(copy.a)
        , b(copy.b)
        , c(copy.c)
        , str2(copy.str2)
    {}

    some_struct& operator=(some_struct const& copy)
    {
        some_base::operator=(copy);
        str1 = copy.str1;
        a    = copy.a;
        b    = copy.b;
        c    = copy.c;
        str2 = copy.str2;
        return *this;
    }

    ~some_struct()
    {}
    // Note the below is pseudo code
    // Also note member destruction happens after user code.
    // In the compiler generated version the user code is empty
        : ~str2()
        // PODs don't have destructor
        , ~str1()
        , ~some_base();
    // End of destructor here.

    // In C++11 we also have Move constructor and move assignment.
    some_struct(some_struct&& copy)
                    //    ^^^^  Notice the double &&
        : some_base(std::move(copy))
        , str1(std::move(copy.str1))
        , a(std::move(copy.a))
        , b(std::move(copy.b))
        , c(std::move(copy.c))
        , str2(std::move(copy.str2))
    {}

    some_struct& operator=(some_struct&& copy)
                               //    ^^^^  Notice the double &&
    {
        some_base::operator=(std::move(copy));
        str1 = std::move(copy.str1);
        a    = std::move(copy.a);
        b    = std::move(copy.b);
        c    = std::move(copy.c);
        str2 = std::move(copy.str2);
        return *this;
    } 
};