C++ constructors and assignment

Hello,

as known, C++ has constructors, copy constructors and assignment operators.

A constructor is called when a custom object is declared or initialized, a copy constructor is called when an object is created, based on another existing object, or when a temporary object has to be created, i.e. an object is passed by-value as a function argument or the function return an object. The copy constructor isn’t called during initialization (although the ‘=’ keyword is used), it is not an assignment operator, anyway.

With modern C++ (from C++11 on) the move constructor and the move assignment operator have been introduced in the family of the essential operations, to optimize the code in certain situations. The copy constructor and the assignment operator create a copy of the original object but, sometimes, this copy is promptly assigned and discarded.

This happens when a function returns a temporary object, that, once assigned, is destroyed: in this case, a deep copy of the temporary object is no-use, and the copy assignment operator can simply copy the pointer (of a location in the heap) from the temporary object to the destination (and clear the pointer in the temporary object).

Another situation is when an object is pushed into a container, and temporary objects are created.

Assignment and copy constructors are automatically created by the compiler with shallow copy operations, that generally work perfectly, except when resources that must be freed (i.e. memory heap) are involved. In these cases, it’s better to override all the essential operations. It is possible to force the compiler to use the default for certain operations by adding =default; to the function declaration or =delete; to forbid its usage (i.e. no copy constructor is allowed);

Test class

To understand the behaviour of different constructors and assignment operators, a text class CPX (similar to a complex number, but clearly uncompleted) has been designed. It contains both members that are allocated on the stack and in the heap, to show how memory management could be done. The class and some global (in the namespace) functions are exported as a module.

export module Cpx;

import std;

using std::cout;
using std::endl;
using std::ostream;
using std::sqrt;
using std::format;
using std::string;
using std::print;

// #define NO_COPY_CTOR
// #define NO_COPY_ASSIGN
// #define NO_MOVE_CTOR
// #define NO_MOVE_ASSIGN

namespace cpx
{

export class CPX
{
private:
	double re;
	double im;
	double *mod = nullptr;

public:
	// Default ctor
	CPX() : re{0.0}, im{0.0}
	{
		mod = new double;
		update();
		#if _DEBUG
		cout << "Ctor CPX() : " << *this << endl;
		#endif
	}

	// Dtor
	~CPX()
	{
		#if _DEBUG
		cout << "Dtor ~CPX() era " << *this << endl;
		#endif
		if(mod != nullptr)
			delete mod;
	}

	// Ctor with one arg
	explicit CPX(double r) : re{r}, im{0.0}
	{
		mod = new double;
		update();
		#if _DEBUG
		cout << "Ctor CPX(double r) : " << *this << endl;
		#endif
	}

	// Ctor with two args
	CPX(double r, double i) : re{r}, im{i}
	{
		mod = new double;
		update();
		#if _DEBUG
		cout << "Ctor CPX(double r, double i) : " << *this << endl;
		#endif
	}

	#ifndef NO_COPY_CTOR
	// Copy ctor
	CPX(const CPX& obj) : re{obj.re}, im{obj.im}
	{
		mod = new double;
		update();
		#if _DEBUG
		cout << "Copy ctor CPX(const CPX& obj) : " << *this << endl;
		#endif
	}
	#endif

	#ifndef NO_COPY_ASSIGN
	// Copy assignment
	CPX& operator=(const CPX& obj) 
	{
		bool autoassign = false;
		bool freeres = false;
		if(this != &obj)		// non autoassegnazione puntatore this != indirizzo di obj
		{
			if(mod!=nullptr)	// libera le attuali risorse
				{
				delete mod;
				freeres = true;
				}
			mod = new double;	// le rialloca
			re = obj.re;		// copia i valori
			im = obj.im;
			*mod = *obj.mod;	// copia il contenuto allocato senza usare update()
		}
		else
		{
			autoassign = true;
		}
		#if _DEBUG
		cout << "Copy assignment CPX& operator=(const CPX& obj) : " << *this;
		if(freeres) cout << " (freeres) ";
		if (autoassign) cout << " (autoassign) ";
		cout << endl;
		#endif
		return *this;
	}
	#endif

	#ifndef NO_MOVE_CTOR
	// Move ctor
	CPX(CPX&& obj) : re{ obj.re }, im{ obj.im }
	{
		mod = obj.mod;				// Copia il puntatore, senza allocare nulla né ricalcolare con update
		obj.mod = nullptr;			// Rimuove il vecchio riferimento
		#if _DEBUG
		cout << "Move ctor CPX(const CPX& obj) : " << *this << endl;
		#endif
		
	}
	#endif

	#ifndef NO_MOVE_ASSIGN
	// Move assignment
	CPX& operator=(CPX&& obj)
	{
		bool autoassign = false;
		bool freeres = false;
		if (this != &obj)			// non autoassegnazione puntatore this != indirizzo di obj
		{
			if (mod != nullptr)		// libera le attuali risorse
			{
				delete mod;
				freeres = true;
			}
			re = obj.re;			// Copia i valori
			im = obj.im;
			mod = obj.mod;			// Copia il puntatore, senza allocare nulla né ricalcolare con update
			obj.mod = nullptr;		// Rimuove il vecchio riferimento
		}
		else
		{
			autoassign = true;
		}

		#if _DEBUG
		cout << "Move assignment CPX& operator=(CPX&& obj) : " << *this;
		if (freeres) cout << " (freeres) ";
		if (autoassign) cout << " (autoassign) ";
		cout << endl;
		#endif
		return *this;
	}
	#endif

	// Accesso
	double Re() const {return re;};
	double Im() const {return im;};

	// Su stream
	friend ostream& operator<<(ostream& os, const CPX& c);

	// Operatori
	friend CPX operator+(CPX& a, CPX& b);
	CPX &operator+=(const CPX& x)
	{
		re+=x.re;
		im+=x.im;
		return *this;
	}
private:
	void update()
		{
		if(mod == nullptr)
		{
			mod = new double;
			cout << "ERROR: mod non allocato" << endl;
		}
		*mod = sqrt( re*re + im*im);
		}
};

export ostream& operator<<(ostream& os, const CPX& c)
{
	os << '(' << c.re << ';' << c.im << ')';
	if(c.mod != nullptr)
	{
		cout << " |"<< std::setprecision(3) << std::fixed << *(c.mod) << std::defaultfloat << std::setprecision(-1) << "|";
	}
	return os;
}

export CPX operator+(CPX& a, CPX& b)
	{
	return CPX(a.re+b.re, a.im+b.im);
	}

}	// fine namespace cpx

Note that the constructor with one argument has been declared explicit: this (although unnecessary) inhibits an initialization after the = sign.

The general constructors, the copy-constructor and the assignment operator do allocate some memory in the heap, while the move constructor and the move assignment operator don’t free the old object memory, simply copy the pointerto the new object (and set it to nullptr in the old one). The destructor provides the allocated memory being released (according to Resource Allocation Is Initialization), but checking the pointer has not been zeroed by a previous move operation.

All the functions, in debug mode, emit a message in the stdout, to make the operation logic clear.

Main program

Different initializations and function calls are executed in the main program:


import std;
import Cpx;

using std::cout;
using std::endl;
using std::vector;

using namespace cpx;

// Prototyping
CPX func1(CPX x);
CPX func2(CPX x);
void func3(CPX &x, const CPX &y);
CPX *func4(CPX &x);

int main()
{
    cout << "PROVA COSTRUTTORI" << endl << endl;

    CPX a;
    CPX b(1);
    // CPX b1 = 1;      // Non permesso: explicit CPX(double r)
    CPX c(1,2);
    CPX c1 = {2,3};     // Permesso: CPX(double r, double i) non explicit
    CPX d{4};
    // CPX d1 = {1};    // Non permesso: explicit CPX(double r)
    CPX e = d;
    CPX f{e};
    CPX g(5,-6);
    CPX h = std::move(g);      // Non usare 'g' di qui in poi
    CPX j = CPX(6,-7);     // Costruisce e inizializza, senza oggetto intermedio e costruttore di copia

    cout << "a=" << a << endl;
    cout << "h=" << h << endl;
    cout << "j=" << j << endl;

    cout << endl << "Chiama func1()..." << endl;
    d = func1(b);
    cout << "Fine func1()." << endl << endl;
    cout << "d=" << d << endl;

    cout << endl << "Chiama func2()..." << endl;
    d = func2(b);
    cout << "Fine func2()." << endl << endl;
    cout << "d=" << d << endl;

    cout << endl << "Chiama func3()..." << endl;
    func3(d,b);
    cout << "Fine func3()." << endl << endl;
    cout << "d=" << d << endl;

    cout << endl << "Chiama func4()..." << endl;
    CPX *dp = func4(b);
    cout << "Fine func4()." << endl << endl;
    cout << "*dp=" << *dp << endl;
    delete dp;

    b = b;
    
    cout << endl << "Vector<CPX>..." << endl;
    vector<CPX> vc;
    vc.push_back(CPX(-2,3));
    vc.push_back({-3,4});
    CPX xv{ -4,6 };
    vc.push_back(xv);

    cout << endl << "Operators..." << endl;
    auto k = b + c;
    cout << "k=" << k << endl;
    j += CPX(1,1);
    cout << "j=" << j << endl;

    cout << endl;
    #if _DEBUG
    cout << "_DEBUG\n";
    #endif
}

CPX func1(CPX x)
{
    return CPX(x.Re() + 10.0, x.Im() + 10.0);
}

CPX func2(CPX x)
{
    CPX tmp(x.Re()+10.0,x.Im()+10.0);
    return tmp;
}

void func3(CPX &x, const CPX &y)
{  
     x = CPX(y);  
}

CPX *func4(CPX &x)
{
    return new CPX(x.Re() + 10.0, x.Im() + 10.0);
}

When the program is executed, it produces this output:

PROVA COSTRUTTORI

Ctor CPX() : (0;0) |0.000|
Ctor CPX(double r) : (1;0) |1.000|
Ctor CPX(double r, double i) : (1;2) |2.236|
Ctor CPX(double r, double i) : (2;3) |3.606|
Ctor CPX(double r) : (4;0) |4.000|
Copy ctor CPX(const CPX& obj) : (4;0) |4.000|
Copy ctor CPX(const CPX& obj) : (4;0) |4.000|
Ctor CPX(double r, double i) : (5;-6) |7.810|
Move ctor CPX(const CPX& obj) : (5;-6) |7.810|
Ctor CPX(double r, double i) : (6;-7) |9.220|
a=(0;0) |0.000|
h=(5;-6) |7.810|
j=(6;-7) |9.220|

Chiama func1()...
Copy ctor CPX(const CPX& obj) : (1;0) |1.000|
Ctor CPX(double r, double i) : (11;10) |14.866|
Dtor ~CPX() era (1;0) |1.000|
Move assignment CPX& operator=(CPX&& obj) : (11;10) |14.866| (freeres)
Dtor ~CPX() era (11;10)
Fine func1().

d=(11;10) |14.866|

Chiama func2()...
Copy ctor CPX(const CPX& obj) : (1;0) |1.000|
Ctor CPX(double r, double i) : (11;10) |14.866|
Dtor ~CPX() era (1;0) |1.000|
Move assignment CPX& operator=(CPX&& obj) : (11;10) |14.866| (freeres)
Dtor ~CPX() era (11;10)
Fine func2().

d=(11;10) |14.866|

Chiama func3()...
Copy ctor CPX(const CPX& obj) : (1;0) |1.000|
Move assignment CPX& operator=(CPX&& obj) : (1;0) |1.000| (freeres)
Dtor ~CPX() era (1;0)
Fine func3().

d=(1;0) |1.000|

Chiama func4()...
Ctor CPX(double r, double i) : (11;10) |14.866|
Fine func4().

*dp=(11;10) |14.866|
Dtor ~CPX() era (11;10) |14.866|
Copy assignment CPX& operator=(const CPX& obj) : (1;0) |1.000| (autoassign)

Vector<CPX>...
Ctor CPX(double r, double i) : (-2;3) |3.606|
Move ctor CPX(const CPX& obj) : (-2;3) |3.606|
Dtor ~CPX() era (-2;3)
Ctor CPX(double r, double i) : (-3;4) |5.000|
Move ctor CPX(const CPX& obj) : (-3;4) |5.000|
Copy ctor CPX(const CPX& obj) : (-2;3) |3.606|
Dtor ~CPX() era (-2;3) |3.606|
Dtor ~CPX() era (-3;4)
Ctor CPX(double r, double i) : (-4;6) |7.211|
Copy ctor CPX(const CPX& obj) : (-4;6) |7.211|
Copy ctor CPX(const CPX& obj) : (-2;3) |3.606|
Copy ctor CPX(const CPX& obj) : (-3;4) |5.000|
Dtor ~CPX() era (-2;3) |3.606|
Dtor ~CPX() era (-3;4) |5.000|

Operators...
Ctor CPX(double r, double i) : (2;2) |2.828|
k=(2;2) |2.828|
Ctor CPX(double r, double i) : (1;1) |1.414|
Dtor ~CPX() era (1;1) |1.414|
j=(7;-6) |9.220|

_DEBUG
Dtor ~CPX() era (2;2) |2.828|
Dtor ~CPX() era (-4;6) |7.211|
Dtor ~CPX() era (-2;3) |3.606|
Dtor ~CPX() era (-3;4) |5.000|
Dtor ~CPX() era (-4;6) |7.211|
Dtor ~CPX() era (7;-6) |9.220|
Dtor ~CPX() era (5;-6) |7.810|
Dtor ~CPX() era (5;-6)
Dtor ~CPX() era (4;0) |4.000|
Dtor ~CPX() era (4;0) |4.000|
Dtor ~CPX() era (1;0) |1.000|
Dtor ~CPX() era (2;3) |3.606|
Dtor ~CPX() era (1;2) |2.236|
Dtor ~CPX() era (1;0) |1.000|
Dtor ~CPX() era (0;0) |0.000|

As you can see, constructors and initializations call copy constructors only when the copy of a non-constant object is required.
The std::move() function forces the compiler to call to the move constructor.

When a function is called, the copy constructor of the arguments (if passed by-value) is called. When returning a value, the move assignment operator is called.

Please note that, if the class is compiled with:

#define NO_MOVE_CTOR
#define NO_MOVE_ASSIGN

the output is quite different, the copy constructor is called, making extra copies even when not strictly necessary:

PROVA COSTRUTTORI

Ctor CPX() : (0;0) |0.000|
Ctor CPX(double r) : (1;0) |1.000|
Ctor CPX(double r, double i) : (1;2) |2.236|
Ctor CPX(double r, double i) : (2;3) |3.606|
Ctor CPX(double r) : (4;0) |4.000|
Copy ctor CPX(const CPX& obj) : (4;0) |4.000|
Copy ctor CPX(const CPX& obj) : (4;0) |4.000|
Ctor CPX(double r, double i) : (5;-6) |7.810|
Copy ctor CPX(const CPX& obj) : (5;-6) |7.810|
Ctor CPX(double r, double i) : (6;-7) |9.220|
a=(0;0) |0.000|
h=(5;-6) |7.810|
j=(6;-7) |9.220|

Chiama func1()...
Copy ctor CPX(const CPX& obj) : (1;0) |1.000|
Ctor CPX(double r, double i) : (11;10) |14.866|
Dtor ~CPX() era (1;0) |1.000|
Copy assignment CPX& operator=(const CPX& obj) : (11;10) |14.866| (freeres)
Dtor ~CPX() era (11;10) |14.866|
Fine func1().

d=(11;10) |14.866|

Chiama func2()...
Copy ctor CPX(const CPX& obj) : (1;0) |1.000|
Ctor CPX(double r, double i) : (11;10) |14.866|
Dtor ~CPX() era (1;0) |1.000|
Copy assignment CPX& operator=(const CPX& obj) : (11;10) |14.866| (freeres)
Dtor ~CPX() era (11;10) |14.866|
Fine func2().

d=(11;10) |14.866|

Chiama func3()...
Copy ctor CPX(const CPX& obj) : (1;0) |1.000|
Copy assignment CPX& operator=(const CPX& obj) : (1;0) |1.000| (freeres)
Dtor ~CPX() era (1;0) |1.000|
Fine func3().

d=(1;0) |1.000|

Chiama func4()...
Ctor CPX(double r, double i) : (11;10) |14.866|
Fine func4().

*dp=(11;10) |14.866|
Dtor ~CPX() era (11;10) |14.866|
Copy assignment CPX& operator=(const CPX& obj) : (1;0) |1.000| (autoassign)

Vector<CPX>...
Ctor CPX(double r, double i) : (-2;3) |3.606|
Copy ctor CPX(const CPX& obj) : (-2;3) |3.606|
Dtor ~CPX() era (-2;3) |3.606|
Ctor CPX(double r, double i) : (-3;4) |5.000|
Copy ctor CPX(const CPX& obj) : (-3;4) |5.000|
Copy ctor CPX(const CPX& obj) : (-2;3) |3.606|
Dtor ~CPX() era (-2;3) |3.606|
Dtor ~CPX() era (-3;4) |5.000|
Ctor CPX(double r, double i) : (-4;6) |7.211|
Copy ctor CPX(const CPX& obj) : (-4;6) |7.211|
Copy ctor CPX(const CPX& obj) : (-2;3) |3.606|
Copy ctor CPX(const CPX& obj) : (-3;4) |5.000|
Dtor ~CPX() era (-2;3) |3.606|
Dtor ~CPX() era (-3;4) |5.000|

Operators...
Ctor CPX(double r, double i) : (2;2) |2.828|
k=(2;2) |2.828|
Ctor CPX(double r, double i) : (1;1) |1.414|
Dtor ~CPX() era (1;1) |1.414|
j=(7;-6) |9.220|

_DEBUG
Dtor ~CPX() era (2;2) |2.828|
Dtor ~CPX() era (-4;6) |7.211|
Dtor ~CPX() era (-2;3) |3.606|
Dtor ~CPX() era (-3;4) |5.000|
Dtor ~CPX() era (-4;6) |7.211|
Dtor ~CPX() era (7;-6) |9.220|
Dtor ~CPX() era (5;-6) |7.810|
Dtor ~CPX() era (5;-6) |7.810|
Dtor ~CPX() era (4;0) |4.000|
Dtor ~CPX() era (4;0) |4.000|
Dtor ~CPX() era (1;0) |1.000|
Dtor ~CPX() era (2;3) |3.606|
Dtor ~CPX() era (1;2) |2.236|
Dtor ~CPX() era (1;0) |1.000|
Dtor ~CPX() era (0;0) |0.000|

Some aspects have not been examined here, but I hope this example could make the C++ mechanism and logic a little clearer.

Best regards !