All about constructors

This post discuss all topics about constructors.

Concept: Rule of three and rule of five

  • The rules basically advise that you should explicitely defines default constructor, copy constructor, move constructor, assignment operator overload, move operator, and destructor.
  • Sample implementation rule of five
class person
{
    std::string name;
    int age;

public:
    person(const std::string& name, int age);        // Ctor
    person(const person &) = default;                // 1/5: Copy Ctor
    person(person &&) noexcept = default;            // 4/5: Move Ctor
    person& operator=(const person &) = default;     // 2/5: Copy Assignment
    person& operator=(person &&) noexcept = default; // 5/5: Move Assignment
    ~person() noexcept = default;                    // 3/5: Dtor
};

Implementation

Implementing copy constructor

  • why do we need? the object is used a Left-Value such as the following cases
    • When an object of the class is returned by value. 
    • When an object of the class is passed (to a function) by value as an argument. 
    • When an object is constructed based on another object of the same class. 
    • When the compiler generates a temporary object.
// return a car from inventory in Feb
// the obj is returned by value, (it is left-value)
Car newCar = FactoryStore.collectInventory() 

// the object is passed to function as an argument, and a copy of Car is made
vector<Car> carCollection;
carCollection.push_back(newCar);
  • what do we consider? shallow copy or deep copy

The following program illustrates that a copy of MyClass will not have independent dynamic memory allocated if deep copy is not implemented.

class MyClass {
    int getA() { return a; };
    void setA(int x) { a = x; };
    int getB() { return b; };
    void setB(int x) { *b = x; };
    int a =0; 
    int * b;
};

int main(){
   MyClass class1;
   class1.setA(5);
   class2.setB(10);
   MyClass class2 = a; 
   std::cout<<class2.getA()<<std::endl;      // will print out 5 because the value is copied
   std::cout<<class2.getB()<<std::endl;      // will print out 10 because the pointer class2->b points to the same memory location as class1->b
   class2.setB(20);
   std::cout<<class2.getB()<<std::endl;      // will print out 20 because the pointer class2->b points to the same memory location as class1->b 
}
  • The following illustrates how to implement deep copy with copy constructor. It also shows the set back with copy constructor.
#include <iostream>
#include <iomanip>
#include <vector>

class MyClass {
private:
    static int count_;
    int id_{ count_++ };
    void log(char const *s) const { 
        std::cout << '[' << this << "]  " << std::left << std::setw(30) << s;
        std::cout << std::right << std::setw((id_ + 1) * 3) << '(' << id_ << ")\n";
    }
    void log(char const *s, int from) const {
        std::string str{ s };
        str += " (from ";
        str += std::to_string(from);
        str += ")";
        log(str.c_str());
    }

public:
    MyClass()                                         { log("Default constructor"); }
    MyClass(MyClass const& s)                         { log("Copy constructor", s.id_); }
    // MyClass(MyClass&& s) noexcept                  { log("Move constructor", s.id_); }
    auto operator= (MyClass const& s)     -> MyClass& { log("Copy Assignment", s.id_); return *this; }
    auto operator= (MyClass&& s) noexcept -> MyClass& { log("Move assignment", s.id_); return *this; }
    ~MyClass()                                        { log("Destructor"); }
};

int MyClass::count_ = 0;

int main()
{
    std::vector<MyClass> vec;
    for (int i = 0; i < 4; i++) {
        std::cout << "index: " << i << "   capacity: " << vec.capacity() << '\n';
        MyClass obj;
        vec.push_back(obj);
    }
    std::cout << "main loop ends\n";
    return 0;
}
index: 0   capacity: 0
[0x7ffeb80fcf58]  Default constructor             (0)
[0x55d9f1d4f2c0]  Copy constructor (from 0)          (1)
[0x7ffeb80fcf58]  Destructor                      (0)
index: 1   capacity: 1
[0x7ffeb80fcf58]  Default constructor                   (2)
[0x55d9f1d4f2e4]  Copy constructor (from 2)                (3)
[0x55d9f1d4f2e0]  Copy constructor (from 1)                   (4)
[0x55d9f1d4f2c0]  Destructor                         (1)
[0x7ffeb80fcf58]  Destructor                            (2)
index: 2   capacity: 2
[0x7ffeb80fcf58]  Default constructor                            (5)
[0x55d9f1d4f2c8]  Copy constructor (from 5)                         (6)
[0x55d9f1d4f2c0]  Copy constructor (from 4)                            (7)
[0x55d9f1d4f2c4]  Copy constructor (from 3)                               (8)
[0x55d9f1d4f2e0]  Destructor                                  (4)
[0x55d9f1d4f2e4]  Destructor                               (3)
[0x7ffeb80fcf58]  Destructor                                     (5)
index: 3   capacity: 4
[0x7ffeb80fcf58]  Default constructor                                        (9)
[0x55d9f1d4f2cc]  Copy constructor (from 9)                                     (10)
[0x7ffeb80fcf58]  Destructor                                                 (9)
main loop ends
[0x55d9f1d4f2c0]  Destructor                                           (7)
[0x55d9f1d4f2c4]  Destructor                                              (8)
[0x55d9f1d4f2c8]  Destructor                                        (6)
[0x55d9f1d4f2cc]  Destructor                                                    (10)

Implementing move constructor

  • The following illustrates basic implementation of move constructor
class MyClass {
    // Move constructor other to new location
    // double && makes r-value reference 
    MyClass (const MyClass && other){   
        a = other.a;
        b = other.b;         // point to the temporary obj
        other.b = nullptr;   // null out the temporary obj   
    }
    int a =0; 
    int * b;
};
// implement the move constructor with double &&
MyClass (const MyClass && other)
// call the move constructor by passing the class directly to function call:
vec.push_back(MyClass());
  • The following shows how changes are made after move constructor is added
#include <iostream>
#include <iomanip>
#include <vector>

class MyClass {
private:
    static int count_;
    int id_{ count_++ };
    void log(char const *s) const { 
        std::cout << '[' << this << "]  " << std::left << std::setw(30) << s;
        std::cout << std::right << std::setw((id_ + 1) * 3) << '(' << id_ << ")\n";
    }
    void log(char const *s, int from) const {
        std::string str{ s };
        str += " (from ";
        str += std::to_string(from);
        str += ")";
        log(str.c_str());
    }

public:
    MyClass()                                         { log("Default constructor"); }
    MyClass(MyClass const& s)                         { log("Copy constructor", s.id_); }
    MyClass(MyClass&& s) noexcept                     { log("Move constructor", s.id_); }
    auto operator= (MyClass const& s)     -> MyClass& { log("Copy Assignment", s.id_); return *this; }
    auto operator= (MyClass&& s) noexcept -> MyClass& { log("Move assignment", s.id_); return *this; }
    ~MyClass()                                        { log("Destructor"); }
};

int MyClass::count_ = 0;

int main()
{
    std::vector<MyClass> vec;
    for (int i = 0; i < 4; i++) {
        std::cout << "index: " << i << "   capacity: " << vec.capacity() << '\n';
        // MyClass obj;
        vec.push_back(MyClass());     // note that passing MyClass obj will not trigger move constructor
    }
    std::cout << "main loop ends\n";
    return 0;
}
  • We have a significant decrease in the number of constructors being called
index: 0   capacity: 0
[0x7ffcc4051268]  Default constructor             (0)
[0x556028cfb2c0]  Move constructor (from 0)          (1)
[0x7ffcc4051268]  Destructor                      (0)
index: 1   capacity: 1
[0x7ffcc4051268]  Default constructor                   (2)
[0x556028cfb2e4]  Move constructor (from 2)                (3)
[0x556028cfb2e0]  Move constructor (from 1)                   (4)
[0x556028cfb2c0]  Destructor                         (1)
[0x7ffcc4051268]  Destructor                            (2)
index: 2   capacity: 2
[0x7ffcc4051268]  Default constructor                            (5)
[0x556028cfb2c8]  Move constructor (from 5)                         (6)
[0x556028cfb2c0]  Move constructor (from 4)                            (7)
[0x556028cfb2e0]  Destructor                                  (4)
[0x556028cfb2c4]  Move constructor (from 3)                               (8)
[0x556028cfb2e4]  Destructor                               (3)
[0x7ffcc4051268]  Destructor                                     (5)
index: 3   capacity: 4
[0x7ffcc4051268]  Default constructor                                        (9)
[0x556028cfb2cc]  Move constructor (from 9)                                     (10)
[0x7ffcc4051268]  Destructor                                                 (9)
main loop ends
[0x556028cfb2c0]  Destructor                                           (7)
[0x556028cfb2c4]  Destructor                                              (8)
[0x556028cfb2c8]  Destructor                                        (6)
[0x556028cfb2cc]  Destructor                                                    (10)

References:

Move constructors in C with examples

Best Move constructor implementation practices.

Understanding the meaning of left and right values.

Also credit to Bob_ who help me with the example on my stackoverflow question.

Implement assignment operator

  • The following illustrates how to implement assignment operator
MyClass& operator=(const MyClass & other) {   // deep copy assignment
    a = other.a;
    other->b = new int();    // allocate new memory location for the pointer
    other->b = *b;           // copy value
}
  • when would assignment operator be useful?

Concept: Singleton

The topic of singleton is discussed in details in another post: Different ways to implement Singleton in C++ – My sky (freewindcode.com)

Leave a Reply

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