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

7

77

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..)

Gamete answered 15/10, 2009 at 21:26 Comment(0)
U
120

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;
    } 
};
Ursal answered 15/10, 2009 at 22:2 Comment(3)
This is an insanely good answer already, but I would love to see an example using the smart pointers. I've never been amazing at auto_ptrWallah
@Hamy: This is the information you need to build the smart pointer. If you are using smart pointers then you don't actually need to worry about this. You only need to worry about the above if you have RAW owned pointers in your class.Ursal
This answer confuses the types of initialization. With no initializer, the struct will be default initialized: its POD-typed members will assume indeterminate values. With an empty initializer, the struct will be value initialized: its POD-typed members will be zero initialized.Bike
C
8

In C++, structs are equivalent to classes where members default to public rather than private access.

C++ compilers will also generate the following special members of a class automatically if they are not provided:

  • Default constructor - no arguments, default initalizes everything.
  • Copy constructor - ie a method with the same name as the class, that takes a reference to another object of the same class. Copies all values across.
  • Destructor - Called when the object is destroyed. By default does nothing.
  • Assignment operator - Called when one struct/class is assigned to another. This is the automatically generated method that's being called in the above case.
Cleareyed answered 15/10, 2009 at 21:36 Comment(2)
An implicit default constructor is also not provided if there is any user-defined constructor.Rehabilitate
An implicit destructor also invokes destructors of members and subobjects (if there are any)Rehabilitate
D
5

That behavior is necessary in order to maintain source compatibility with C.

C does not give you the ability to define/override operators, so structs are normally copied with the = operator.

David answered 15/10, 2009 at 21:31 Comment(2)
K&R C didn't allow for structures to be copied with = at all, and I'm not sure about C89. If it was introduced in C99, then I'd argue that it was due to C++ influence.Staple
According to K&R (2nd edition, 1988, p. 127) it was introduced by ANSI C but most existing compilers already supported it.David
H
4

But it is defined. In the standard. If you supply no operator =, one is supplied to you. And the default operator just copies each of the member variables. And how does it know which way to copy each member? it calls their operator = (which, if not defined, is supplied by default...).

Holzman answered 15/10, 2009 at 21:30 Comment(0)
P
3

The assignment operator (operator=) is one of the implicitly generated functions for a struct or class in C++.

Here is a reference describing the 4 implicitly generated members:
http://www.cs.ucf.edu/~leavens/larchc++manual/lcpp_136.html

In short, the implicitly generated member performs a memberwise shallow copy. Here is the long version from the linked page:

The implicitly-generated assignment operator specification, when needed, is the following. The specification says that the result is the object being assigned (self), and that the value of the abstract value of self in the post-state self" is the same as the value of the abstract value of the argument from.

// @(#)$Id: default_assignment_op.lh,v 1.3 1998/08/27 22:42:13 leavens Exp $
#include "default_interfaces.lh"

T& T::operator = (const T& from) throw();
//@ behavior {
//@   requires assigned(from, any) /\ assigned(from\any, any);
//@   modifies self;
//@   ensures result = self /\ self" = from\any\any;
//@   ensures redundantly assigned(self, post) /\ assigned(self', post);
//           thus
//@   ensures redundantly assigned(result, post) /\ assigned(result', post);
//@ }
Probe answered 15/10, 2009 at 21:35 Comment(2)
The default assignment operator can't throw because it doesn't allocate any memory. :dunno:Probe
@Rob: The definition of the default copy assignment operator starting at 12.8:10 makes no mention of a throw clause. This makes sense to me, since a default copy assignment operator can call a non-default assignment, which could throw. In the specific example given in the question obviously std::string::operator=(const std::string&) can throw.Outshine
N
2

The compiler will synthesise some members for you if you don't define them explicitly yourself. The assignment operator is one of them. A copy constructor is another, and you get a destructor too. You also get a default constructor if you don't provide any constructors of your own. Beyond that I'm not sure what else but I believe there may be others (the link in the answer given by 280Z28 suggests otherwise though and I can't remember where I read it now so maybe it's only four).

Narcotic answered 15/10, 2009 at 21:28 Comment(0)
S
0

structs are basically a concatenation of its components in memory (with some possible padding built in for alignment). When you assign one struct the value of another, the values are just coped over.

Stagnate answered 15/10, 2009 at 21:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.