Why does the = operator work on structs without having been defined?
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..)
Solution 1:
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;
}
};
Solution 2:
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.
Solution 3:
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.
Solution 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...).
Solution 5:
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 ofself
in the post-stateself
" is the same as the value of the abstract value of the argumentfrom
.
// @(#)$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);
//@ }