Home | Projects | Notes > C++ Programming > Constructors & Destructors
Special member method
Invoked during object creation
Useful for initialization (to a known/safe/stable state)
Has same name as the class
No return type is specified
Can be overloaded
Player class
xxxxxxxxxx121class Player2{3private:4 std::string name;5 int health;6 int xp;7public:8 // Overloaded constructors9 Player();10 Player(std::string name);11 Player(std::string name, int health, int xp);12};Account class
xxxxxxxxxx121class Account2{3private:4 std::string name;5 double balance;6public:7 // Constructors8 Account();9 Account(std::string name, double balance);10 Account(std::string name);11 Account(double balance);12};
Special member method
Has same name as the class except that it is proceeded with a tilde (~)
Invoked automatically when an object is destroyed (When an object goes out of its scope or a pointer pointing to an object gets deleted)
It is a great place to release memory, close files and free up any other resources.
No return type and no parameters
Only 1 destructor is allowed per class - CANNOT be overloaded!
Doesn't make sense to overload a destructor since it is called automatically by C++.
Useful to release memory and other resources
Player class
xxxxxxxxxx141class Player2{3private:4 std::string name;5 int health;6 int xp;7public:8 // Overloaded constructors9 Player();10 Player(std::string name);11 Player(std::string name, int health, int xp);12 // Destructor13 ~Player();14};xxxxxxxxxx91{2 // The following 3 objects are all local and get created on stack memory3 Player jack;4 Player sunny {"Sunny", 100, 4};5 Player yena {"Yena"};6} // 3 destructors called7
8Player *kj = new Player("KJ", 1000, 0);9delete enemy; // Destructor calledAccount class
xxxxxxxxxx141class Account2{3private:4 std::string name;5 double balance;6public:7 // Constructors8 Account();9 Account(std::string name, double balance);10 Account(std::string name);11 Account(double balance);12 // Destructor13 ~Account();14};
A constructor that does not expect any arguments (Also, called the no-args constructor)
If you do NOT provide any constructors at all for a class, C++ will generate a default constructor that does nothing.
Since the compiler generated default constructor does nothing, the class member attributes could contain garbage value because they haven't been initialized.
Called when you instantiate a new object with no arguments
It's a best practice to provide a user-defined constructor that ensures objects are instantiated in a known, stable (safe) state.
Once the user provides a user-defined constructor for a class, C++ will not generate the no-args default constructor automatically. If you still need it, then you must explicitly define it yourself.
Player class
xxxxxxxxxx21Player jack; // Creates an object on the stack2Palyer *sunny = new Player; // Creates an object on the heapAccount class
With default constructor
xxxxxxxxxx151class Account2{3private:4 std::string name;5 double balance;6public:7 // Default constructor8 Account()9 {10 name = "None";11 balance = 0.0;12 }13 bool withdraw(double amount);14 bool deposit(double amount);15};xxxxxxxxxx51Account jack_account;2Account yena_account;3
4Account *sunny_account = new Account;5delete sunny_account;With a user-defined constructor (No default constructor)
xxxxxxxxxx151class Account2{3private:4 std::string name;5 double balance;6public:7 // No default constructor8 Account(std::string name_val, double bal)9 {10 name = name_val;11 balance bal;12 }13 bool withdraw(double amount);14 bool deposit(double amount);15};xxxxxxxxxx71Account jack_account; // Error2Account yena_account; // Error3
4Account *sunny_account = new Account; // Error5delete sunny_account;6
7Account kj_account {"KJ", 15000.0}; // OKErrors because the default constructor is no longer automatically generated by the compiler.
Classes can have as many constructors as necessary
Each must have a unique signature
Default constructor is no longer compiler-generated once another constructor is declared
It is important that we initialize our objects to some state that we know. We don't want garbage data in any of the attributes after they are created.
Player class
xxxxxxxxxx331class player2{3private:4 std::string name;5 int health;6 int xp;7public:8 // Overloaded constructors9 Player();10 Player(std::string name_val);11 Player(std::string name_val, int health_val, int xp_val);12};13
14Player::Player()15{16 name = "None";17 health = 0;18 xp = 0;19}20
21Player::Player(std::string name_val)22{23 name = name_val;24 health = 0;25 xp = 0;26}27
28Player::Player(std::string name_val, int health_val, int xp_val)29{30 name = name_val;31 health = health_val;32 xp = xp_val;33}L16: For the record, by the time that we get to this statement, the
namestring object has already been constructed and initialized to an empty string. So we're really just assigning a new string to it, in this case, to an existing object. This is, in reality, different from initialization.
See "Constructor initialization lists" section.
xxxxxxxxxx61Player empty; // None, 0, 02Player hero("Hero"); // Hero, 0, 03Player villain("Villain"); // Villain, 0, 04Player frank {"Frank", 100, 4}; // Frank, 100, 45Player *enemy = new Player("Enemy", 1000, 0); // Enemy, 1000, 06delete enemy;
So far, all data member values have been set in the constructor body. While this works, it technically isn't initialization because by the time the constructor body is executed these member attributes have already been created. So, we're really just assigning values to already created attributes.
Constructor initialization lists help initialize the member data values to specified values BEFORE the constructor body executes. (True initialization!)
Constructor initialization lists
Are more efficient
Immediately follows the parameter list
Initializes the data members as the object is created
Order of initialization is the order of their declarations in the class
The order specified in the initialization list does not matter!
Player class
xxxxxxxxxx241class player2{3private:4 std::string name;5 int health;6 int xp;7public:8 // Overloaded constructors9 Player();10 Player(std::string name_val);11 Player(std::string name_val, int health_val, int xp_val);12};13
14Player::Player()15 : name{"None"}, health{0}, sp{0} // Initialization list16{ }17
18Player::Player::Player(std::string name_val)()19 : name{name_val}, health{0}, sp{0} // Initialization list20{ }21
22Player::Player(std::string name_val, int health_val, int xp_val)23 : name{name_val}, health{health_val}, xp{xp_val}; // Initialization list24{ }Using constructor initialization lists, we can be sure that our data members have been initialized to our own values before any code in the constructor body is executed.
True initialization!
Often the code for constructors is very similar. Many times only the initialization values of the data members is what chages.
Duplicated code can lead to errors, so we want to minimize the amount of duplicated code in our programs.
C++ allows delegating constructors
Code for one constructor can call another in the initialization list
Avoids duplicating code
Player class
xxxxxxxxxx241class player2{3private:4 std::string name;5 int health;6 int xp;7public:8 // Overloaded constructors9 Player();10 Player(std::string name_val);11 Player(std::string name_val, int health_val, int xp_val);12};13
14Player::Player(std::string name_val, int health_val, int xp_val)15 : name{name_val}, health{health_val}, xp{xp_val}; // Initialization list16{ }17
18Player::Player()19 : Player{"None", 0, 0} // Delegating constructor20{ }21
22Player::Player::Player(std::string name_val)()23 : Player{name_val, 0, 0} // Delegating constructor24{ }In this case, the no-args constructor is the delegating constructor, since it delegates object initialization to another constructor.
Can often simplify our code and reduce the number of overloaded constructors by providing default constructor parameters.
Same rules apply as we learned with non-member functions
Player class
We can simply have one constructor that does everything that we did with the three constructors in the previous example.
xxxxxxxxxx211class player2{3private:4 std::string name;5 int health;6 int xp;7public:8 // Constructor with default parameter values9 Player(std::string name_val = "None",10 int health_val = 0,11 int xp_val = 0);12};13
14Player::Player(std::string name_val, int health_val, int xp_val)15 : name{name_val}, health{health_val}, xp{xp_val}16{ }17
18Player empty; // None, 0, 019Player jack{"Jack"}; // Jack, 0, 020Player yena{"Yena", 100, 55}; // Yena, 100, 5521Player hero{"Hero", 100}; // Hero, 100, 0Notice that you don't supply the default parameters here. You can simply use an initializer list to initialize the class attributes, and the compiler will take care of providing the default values as needed.
This is very handy and results in less code to write, which means less code to test, which means less code that could have errors.
Since the single constructor is doing the job of several constructors, we have to be careful not to declare ambiguous constructors. For example, what would happen if you declared a no-args constructor? The compiler will generate an error since it doesn't know which one to call if no arguments were provided when creating an object.
When objects are copied C++ must create a new object from an existing object, and it uses "copy constructor" to do this. (Copy constructors are called whenever the compiler needs to make a copy of an object.)
When is a copy of an object made?
Passing object by value as a parameter
Returning an object from a function by value
Constructing one object based on another of the same class
C++ must have a way of accomplishing this so it provides a compiler-defined copy constructor if you don't. (If you don't provide your own way of copying objects by value then the compiler provides a default way of copying objects.)
Copies the values of each data member to the new object
If you do not provide a user-defined copy constructor, C++ will generate a default copy constructor that will do member-wise copy.
Perfectly fine in many cases, but beware if you have a pointer data member,the pointer will be copied not what it is pointing to.
Shallow copy vs. Deep copy
Best practices
Provide a copy constructor when your class has raw pointer members
Provide the copy constructor with a const reference parameter
Use STL classes as member attributes as they already provide copy constructors
Avoid using raw pointer data members if possible or use smart pointers.
Copy constructor signature
xxxxxxxxxx41Type::Type(const Type &source);2
3Player::Player(const Player &source);4Account::Account(const Account &source);Should pass the source object by constant reference!
Why pass by reference?
If we pass it in by value, then the passed object will get copied along the way, which is the whole point of the copy constructor. We would defeat the purpose and end up with never ending recursive calls. Why constant?
Since we are copying the source without modifying it.
Pass object by value
xxxxxxxxxx101Player hero{"Hero", 100, 20};2
3void display_player(Player p)4{5 // p is a COPY of hero in this example (copy constructor does the copy)6 // Use p7 // Destructor for p will be called8}9
10display_player(hero);Return object by value
xxxxxxxxxx91Player enemy;2
3Player create_super_enemy()4{5 Player an_enemy{"Super Enemy", 100, 100};6 return an_enemy; // A COPY of an_enemy is returned (copy constructor does the copy)7}8
9enemy = create_super_enemy();Here, a copy of
an_enemyobject is made by the copy constructor.
Construct one object based on another
xxxxxxxxxx21Player hero{"Hero", 100, 100};2Player another_hero{hero}; // A COPY of hero is made using the copy constructorDeclaring and implementing a copy constructor
xxxxxxxxxx81// Declaration2Type::Type(const Type &source);3
4// Implementation5Type::Type(const Type &source)6{7 // Code or initialization list to copy the object8}xxxxxxxxxx121// Declaration2Player::Player(const Player &source);3Account::Account(const Account &source);4
5// Implementation6Player::Player(const Player &source)7 : name{source.name}, health{source.health}, xp{source.xp}8{ }9
10Account::Account(const Account &source)11 : name{source.name}, balance{source.balance}12{ }
One of C++11's most pervasive features was probably move semantics. And, to really understand move semantics, it's important to be able to tell whether an expression is an l-value or an r-value.
Rule of thumb:
L-value
When you can refer to an object by name or you can follow a pointer to get to an object, then that object is addressable and it's an l-value.
R-value
Everything else
In context of move semantics r-values refer to temporary objects that are created by the compiler, and objects returned from methods.
Sometimes when we execute code the compiler creates unnamed temporary values:
xxxxxxxxxx21int total{0};2total = 100 + 200;
100 + 200is evaluated and300stored in an unnamedtempvalue (This value is not addressable so it's an r-value.)The
300(r-value) is then stored in the variabletotal(l-value).Then the
tempvalue is discarded
The same happens with objects as well. However, with objects there can be a great amount of overhead if copy constructors are called over and over again making copies of these temporary objects. And, when we have raw pointers and we have to do deep copies then the overhead is even greater.
This is where move semantics and the move constructor comes into picture.
Sometimes copy constructors are called many times automatically due to the copy semantics of C++.
Copy constructors doing deep copying can have a significant performance bottleneck.
Deep copy must be implemented if a class contains a raw poiner.
This is computationally expensive since we have to allocate space for the copy and then copy the data over.
C++11 introduced move semantics and the move constructor (Extremely efficient!)
Move constructor moves an object rather than copy it.
Move constructors are optional but recommended when you have a raw pointer.
If you don't provide them, then the copy constructors will be called.
Copy elision - C++ may optimize copying away completely (RVO - Return Value Optimization)
You may not even see the copy constructors being called. If you experience this, it's probably due to something called copy elision.
Copy elision is a compiler optimization technique that eliminates unnecessary copying. Compilers are really smart with their optimizations now. And one of the common techniques is called "Return Value Optimization". That's when the compiler generates code that doesn't create a copy every return value from a function making the code much more efficient.
Instead of making a deep copy of the resource
It simply moves the resource on the heap
Simply copies the address of the resource from source to the current object
And, nulls out the pointer in the source pointer.
Very efficient since not doing a copy at all.
In the context of move semantics, think of r-value references as references to temporary objects.
Used in moving semantics and perfect forwarding
Move semantics is all about r-value references
Used by move constructor and move assignment operator to efficiently move an object rather than copy it.
R-value reference operator (&&)
This is unlike the l-value reference operator (&)
R-value references
xxxxxxxxxx51// Syntax2Type::Type(Type &&source);3
4Player::Player(Player &&source);5Move::Move(Move &&source);Difference from copy constructors?
There's no
constqualifier for the parameter source because we need to modify it in order to null out its pointer.Parameter is an r-value reference
xxxxxxxxxx91int x{100};2
3int &l_ref = x; // L-value reference (since 'x' is an l-value, addressable, got a name)4l_ref = 10; // Change x to 105
6int &&r_ref = 200; // R-value reference7r_ref = 300; // Change r_ref to 300 (just changed the temporary variable value!)8
9int &&x_ref = x; // Compiler error (Cannot assign an l-value to an r-value reference)R-value reference parameters
xxxxxxxxxx81int x{100}; // x is an l-value2
3void func(int &&num); // B4
5func(200); // Calls B (200 is an r-value)6func(x); // Error (x is an l-value)7 // - Cannot bind r-value reference of type 'int&&' to 8 // an l-value of type 'int'L-value reference parameters
xxxxxxxxxx81int x{100}; // x is an l-value2
3void func(int &num); // A4
5func(x); // Calls A (x is an l-value)6func(200); // Error (200 is an r-value)7 // - Cannot bind non-const l-value reference of type 'int&' to 8 // an r-value of type 'int'L-value and r-value reference parameters
xxxxxxxxxx71int x{100}; // x is an l-value2
3void func(int &num); // A4void func(int &&num); // B5
6func(x); // Calls A (x is an l-value)7func(200); // Calls B (200 is an r-value)Move class WITHOUT move constructor
xxxxxxxxxx181class Move2{3private:4 int *data;5public:6 void set_data_value(int d) { *data = d; }7 int get_data_value() { return *data }8 Move(int d); // Constructor9 Move(const Move &source); // Copy constructor10 ~Move(); // Destructor11};12
13// Copy constructor (Deep copy)14Move::Move(const Move &source)15{16 data = new int;17 *data = *source.data;18}The move constructor 'steals' the data and nulls out the source pointer.
Inefficient copying (Copy constructors will be called to copy the temps)
xxxxxxxxxx41vector<Move> vec;2
3vec.push_back(Move{10});4vec.push_back(Move{20});
Move{10}andMove{20}create temporary objects which are unnamed and r-values. The compiler is going to use the copy constructor to make copies of these.Sample output:
xxxxxxxxxx111Constructor for: 102Constructor for: 103Copy constructor (deep copy) for: 104Destructor freeing data for: 105Constructor for: 206Constructor for: 207Copy constructor (deep copy) for: 208Constructor for: 109Copy constructor (deep copy) for: 1010Destructor freeing data for: 1011Destructor freeing data for: 20Copy constructor does several deep copies, which is very inefficient.
Move class WITH move constructor
xxxxxxxxxx261class Move2{3private:4 int *data;5public:6 void set_data_value(int d) { *data = d; }7 int get_data_value() { return *data }8 Move(int d); // Constructor9 Move(const Move &source); // Copy constructor10 Move(Move &&source); // Move constructor11 ~Move(); // Destructor12};13
14// Copy constructor (Deep copy)15Move::Move(const Move &source)16{17 data = new int;18 *data = *source.data;19}20
21// Move constructor22Move::Move(Move &&source)23 : data{source.data}24{25 source.data = nullptr; // Null out source's pointer26}Efficient copying (Move constructors will be called for the temp r-values)
xxxxxxxxxx41vector<Move> vec;2
3vec.push_back(Move{10});4vec.push_back(Move{20});xxxxxxxxxx101Constructor for: 102Move constructor (moving resource): 103Destructor freeing data for nullptr4Constructor for: 205Move constructor (moving resource): 206Move constructor (moving resource): 107Destructor freeing data for nullptr8Destructor freeing data for nullptr9Destructor freeing data for: 1010Destructor freeing data for: 20No copy constructor is called at all. Instead, move constructor is being called.
Also, note that the destructors are called for
nullptr. That's destroying the object that we just moved and set its data pointer tonullptr.