Constructors and Destructors

We've seen these a bit before, but let's take a closer look.

The constructor is called when an instance of the class is created, and the destructor when the instance is destroyed. If the user does not define any, they are auto-generated by the compiler. In C++, we have the "normal" constructor, copy constructor, move constructor and destructor. We also have move and copy assignment which are overloads of operator=.

If you define none of these functions, the compiler will generate them for you. If you write any constructor (copy, move, or direct) then the compiler will not generate the default (no parameter) constructor. Furthermore, auto generation of copy constructor and copy assignment is deprecated if a user defines a destructor or another copy operation. The compiler may still generate them however, and this is to preserve backwards compatibility since prior to C++11 these auto generation rules weren't enforced. Auto generation of all constructors and assignment operations are forbidden when a move constructor or move assignment is defined. Finally, move constructors are auto generated only if the class contains no other copy or move methods, destructors, or constructors. Since move operations are a C++11 feature, defining them indicates that the class isn't legacy code, and the compiler can enforce auto generation rules.

Generally, if you need to write 1 of the resource management functions, your class's resource handling is probably nontrivial, and you should probably write or explicitly delete the rest.

class One {
public:
    One(int a);
    // move constructor and assignment not generated
    // copy assignment and constructor gen deprecated
    // no gen of default constructor
};

class Two {
public:
    ~Two();
    // move constructor and assignment not generated
};

class Three {
    // all auto gen done
};

class Four {
public:
    Four(const Four & f); //copy constructor
    // no move ops gen 
    // no default ctor
    // deprecated copy assignment
};

class Five {
public:
    Five& operator=(const Five & other); //copy assignment
    // no move ops
    // deprecated copy gen
};

class Six {
public:
    Six(Six && other); //move constructor, more on this later
    // nothing else generated
};

Constructors are called to create a new object. Assignment operators are called to update an existing object.

{
    MyInteger myInt(10); // direct constructor
    MyInteger myInt2 = myInt; // copy constructor, myInt2 is a new instance
    MyInteger myInt3(myInt); // also copy constructor
    myInt3 = myInt2; // copy assignment, myInt3 already exists
} // myInt, myInt2, and myInt3 go out of scope here -> call destructors of all of them

There are cases when we need to define an operation because we defined a function that blocks its generation yet we don't need any special logic. For that, as we have already seen, we can use = default.

class MyClass {
public:
    MyClass(MyClass && other) { /* .. */}
    // all other operation generation prohibited

    MyClass() = default;
    MyClass& operator=(const MyClass &) = default;
    MyClass(const MyClass &) = default;
    MyClass& operator=(MyClass &&) = default; // move assignment (more on this later)

};

On a slightly similar note, if generation is prohibited and we don't want our class to have those functions (or even if generation isn't forbidden but we just don't want them), we should explicitly = delete them to make our intentions clear.

class Class2 {
public:
    Class2(const Class2 & other) { /* ... */ }
    // default ctor and move ops not generated

    Class2() = default;

    Class2& operator=(Class2 &&) = delete;
    Class2(Class2 &&) = delete;
    Class2& operator=(const Class2 &) = delete;
};

Pre C++11, you can imitate this effect by defining functions you didn't want as private. Don't do that, but if you come across legacy code that uses this pattern now you know what it's doing.

Move Basics

Now I talk about this move constructor. But what is moving exactly? Well, instead of performing a copy, we move the internals from one object to another, leaving behind an empty shell of an object to be destroyed. When the old object is destroyed, nothing happens because the object's internal state has been moved out and put into a new object.

The double ampersand is an rvalue reference, and basically it is a reference to temporary values. For example, the return value of a function is moved (well, sometimes, more on this later) into a temporary value and that temporary is returned. The temporary gets destroyed at the end of the statement that called the function. We can also manually move non-temporaries with the std::move function. However, once a value has been moved, you must not use it again since all of its state has been put into a different object.

Now frankly, I've told you a flat out lie, but we'll discuss this in way more detail later.

std::string getString() {
    return "Hello";
}

std::string greet(std::string && name) {
    return "Hello " + name;
}

std::string myStr = getString(); // move constructor
std::string myStr2 = std::move(myStr); // move constructor again
const auto myStr3 = myStr2; // copy constructor

std::string myName = "Jacob";
auto greeting = greet(std::move(myName));
    // move ctor for name in the greet() function
    // move ctor for greeting