Constructors in C++

Introduction

Constructor are a fundamental concept in C++, providing a means to initialize objects and allocate necessary resources when they are created.

The Basics of Constructors:

A constructor is a special member function that is automatically invoked when an object is created. Its primary purpose is to initialize the member variables and set it up for use. In C++, constructors share the same name as the class or structure they belong to and have no return type, not even void.

// Class with a basic constructor
class MyClass {
public:
    // Constructor
    MyClass() {
        // Initialization code goes here
    }
};

int main() {
    MyClass obj; // Object creation triggers the constructor

    return 0;
}

Naming Constructors

Unlike normal functions, constructors have specific rules for how they must be named:

  • Constructors must have the same name as the class (with the same capitalization).
  • Constructors have no return type (not even void).

Types of Constructors:

Constructors in C++ can be categorized into several types based on their characteristics and usage.

1️⃣ Default Constructors:

  • Automatically generated if no constructor is defined.
  • Initializes members to default values (zero for built-in types).
  • Default constructor is the one that accepts no arguments.
class Point {
public:
    // Default Constructor
    Point() : x(0), y(0) {}

    // Parameterized Constructor
    Point(int x, int y) : x(x), y(y) {}

private:
    int x, y;
};

int main() {
    Point defaultPoint; // Invokes default constructor
    Point customPoint(3, 4); // Invokes parameterized constructor

    return 0;
}

2️⃣ Parameterized Constructors:

  • Accept parameters for custom initialization.
  • Enable object creation with specific values.
class Circle {
public:
    Circle(double radius) : radius(radius) {} // Parameterized constructor
private:
    double radius;
};

int main() {
    Circle smallCircle(2.5); // Invokes parameterized constructor

    return 0;
}

3️⃣ Copy Constructors:

  • Creates a new object as a copy of an object.
  • Called when an object is passed by value or returned from a function.
  • A copy constructor is a constructor that is used to initialize an object with an existing object of the same type. After the copy constructor executes, the newly  created object should be a copy of the object passed in as the initializer.
class String {
public:
    String(const String& other) : data(other.data) {} // Copy constructor
private:
    std::string data;
};

int main() {
    String original("Hello");
    String copy(original); // Invokes copy constructor

    return 0;
}

Copy Constructor is Called When?

1 Object Initialization (Direct or Indirect)

When a new object is initialized using an existing object:

MyClass obj1;           // Default constructor
MyClass obj2 = obj1;    // Copy constructor called
  • A new object (obj2) is being created, an it is initialized with the data of obj1.

2 Passing an Object by Value to a Function

When an object is passed to a function by value, the copy constructor is called to create a copy of the argument:

void display(MyClass obj); // Function accepting MyClass by value

MyClass obj1;
display(obj1);             // Copy constructor called
  • The function creates a local copy of the passed object to work with.

3 Returning an Object by Value from a Function

When a function returns an object by value, the copy constructor is called to create a temporary copy of the returned object:

MyClass createObject() {
    MyClass temp;
    return temp;           // Copy constructor called
}

MyClass obj = createObject(); // Copy constructor called
  • The temporary object (temp) is copied to the object receiving the return value (obj).

4 Explicitly Invoking the Copy Constructor

You can explicitly call the copy constructor to create a copy of an object:

MyClass obj1;
MyClass obj2(obj1);        // Copy constructor called

5 Throwing and Catching Objects (Exceptions)

When an object is thrown as an exception and caught by value, the copy constructor is called:

try {
    throw MyClass();
} catch (MyClass obj) {    // Copy constructor called
}

An implicit copy constructor

If you do not provide  a copy constructor for your classes, C++ will create a public implicit copy constructor for you. For example:

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // Default constructor
    Fraction(int numerator=0, int denominator=1)
        : m_numerator{numerator}, m_denominator{denominator}
    {
    }

    void print()
    {
        std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
    }
};

int main()
{
    Fraction f { 5, 3 };  // Calls Fraction(int, int) constructor
    Fraction fCopy { f }; // What constructor is used here?

    f.print();
    fCopy.print();

    return 0;
}

// Output
Fraction(5, 3)
Fraction(5, 3)

In the above example, the statement Fraction fCopy {  f }; is invoking the implicit copy constructor to initialize fCopy with f.

By default, the implicit copy constructor will do memberwise initialization. This means each member will be initialized using the corresponding member of the class passed in as the initializer. In the example above, fCopy.m_numerator is initialized using f.m_numerator (which has value 5), and fCopy.m_denominator is initialized using f.m_denominator (which has value 3).

After the copy constructor has executed, the members of f and fCopy have the same values, so fCopy is a copy of f. Thus calling print() on either has the same result.

Explicitly Defined Copy Constructor

We can also explicitly define our own copy constructor.

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // Default constructor
    Fraction(int numerator=0, int denominator=1)
        : m_numerator{numerator}, m_denominator{denominator}
    {
    }

    // Copy constructor
    Fraction(const Fraction& fraction)
        // Initialize our members using the corresponding member of the parameter
        : m_numerator{ fraction.m_numerator }
        , m_denominator{ fraction.m_denominator }
    {
        std::cout << "Copy constructor called\n"; // just to prove it works
    }

    void print() const
    {
        std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
    }
};

int main()
{
    Fraction f { 5, 3 };  // Calls Fraction(int, int) constructor
    Fraction fCopy { f }; // Calls Fraction(const Fraction&) copy constructor

    f.print();
    fCopy.print();

    return 0;
}

Output of this program would be:

Copy constructor called
Fraction(5, 3)
Fraction(5, 3)

The copy constructor we defined above is functionally equivalent to the one we would get by default, except we have added an output statement to prove the copy constructor is actually being called. This copy constructor is invoked when fCopy is initialized with f.

The Copy Constructor's Parameter must be a reference

It is a requirement that the parameter of a copy constructor be an lvalue reference or const lvalue reference. Because the copy constructor should not be modifying the parameter, using a const lvalue reference is preferred.

Constructors in Structures:

While structures in C++ are similar to classes, they have some differences in terms of member access and inheritance. Constructors in structures are used similarly to constructors in classes.

#include <iostream>

struct Point {
    // Constructor in a structure
    Point(int x, int y) : x(x), y(y) {}

    int x, y;
};

int main() {
    Point myPoint(5, 8);
    std::cout << "Coordinates: (" << myPoint.x << ", " << myPoint.y << ")" << std::endl;

    return 0;
}

Constructors with default arguments

As with all functions, the rightmost parameters of constructors can have default arguments.

#include <iostream>

class Foo
{
private:
    int m_x { };
    int m_y { };

public:
    Foo(int x=0, int y=0) // has default arguments
        : m_x { x }
        , m_y { y }
    {
        std::cout << "Foo(" << x << ", " << y << ") constructed\n";
    }
};

int main()
{
    Foo foo1{};     // calls Foo(int, int) constructor using default arguments
    Foo foo2{6, 7}; // calls Foo(int, int) constructor

    return 0;
}

This prints:

Foo(0, 0) constructed
Foo(6, 7) constructed

Overloaded constructors

Because constructors are functions, they can be overloaded. That is, we can have multiple constructors so that we can construct objects in different ways:

#include <iostream>

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo() // default constructor
    {
        std::cout << "Foo constructed\n";
    }

    Foo(int x, int y) // non-default constructor
        : m_x { x }, m_y { y }
    {
        std::cout << "Foo(" << x << ", " << y << ") constructed\n";
    }
};

int main()
{
    Foo foo1{};     // Calls Foo() constructor
    Foo foo2{6, 7}; // Calls Foo(int, int) constructor

    return 0;
}

If more than one default constructor is provided, the compiler will be unable to disambiguate which should be used:

#include <iostream>

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo() // default constructor
    {
        std::cout << "Foo constructed\n";
    }

    Foo(int x=1, int y=2) // default constructor
        : m_x { x }, m_y { y }
    {
        std::cout << "Foo(" << x << ", " << y << ") constructed\n";
    }
};

int main()
{
    Foo foo{}; // compile error: ambiguous constructor function call

    return 0;
}

In the above example, we instantiate foo with no arguments, so the compiler will look for a default constructor. It will find two, and be unable to disambiguate with constructor should be used. This will result in a compile error.

An implicit default constructor

If a non-aggregate class type object has no user-declared constructors, the compiler will generate a public default constructor (so that the class can be value or default initialized). This constructor is called an implicit default constructor.

Consider the following example:

#include <iostream>

class Foo
{
private:
    int m_x{};
    int m_y{};

    // Note: no constructors declared
};

int main()
{
    Foo foo{};

    return 0;
}

This class has no user-declared constructors, so the compiler will generate an implicit default constructor for us. That constructor will be used to instantiate foo{}.

The implicit default constructor is equivalent to as constructor that has no parameters, no member initializer list, and no statements in the body of the constructor. In other words, for the above Foo class, the compiler generates this:

public:
    Foo() // implicitly generated default constructor
    {
    }

Using = default to generate a default constructor

In cases where we would write a default constructor that is equivalent to the implicitly generated default constructor, we can instead tell the compiler to generate an implicit default constructor for us by using the following syntax:

#include <iostream>

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo() = default; // generate an implicit default constructor

    Foo(int x, int y)
        : m_x { x }, m_y { y }
    {
        std::cout << "Foo(" << x << ", " << y << ") constructed\n";
    }
};

int main()
{
    Foo foo{}; // calls Foo() default constructor

    return 0;
}

Delegating Constructor

Constructors are allowed to delegate (transfer responsibility for) initialization to another constructor from the same class type. This process is sometimes called constructor chaining and such constructors are called delegating constructors.

To make one constructor delegate initialization to another constructor, simply call the constructor in the member initializer list.

#include <iostream>
#include <string>
#include <string_view>

class Employee
{
private:
    std::string m_name{};
    int m_id{ 0 };

public:
    Employee(std::string_view name)
        : Employee{ name, 0 } // delegate initialization to Employee(std::string_view, int) constructor
    {
    }

    Employee(std::string_view name, int id)
        : m_name{ name }, m_id{ id } // actually initializes the members
    {
        std::cout << "Employee " << m_name << " created\n";
    }

};

int main()
{
    Employee e1{ "James" };
    Employee e2{ "Dave", 42 };
}

Constructor Calling in inheritance

Constructor calling in inheritance is a concept in object oriented programming where the constructor of a derived class invokes the constructor of its base class. This process ensures that the base class initializes its part of the derived class object before the derived class adds its own features. The order in which constructors are called is determined by the inheritance hierarchy.

Consider the following example:

#include <iostream>

// Base class
class Base {
public:
    // Parameterized constructor in the base class
    Base(int value) : baseValue(value) {
        std::cout << "Base class constructor called with value: " << baseValue << std::endl;
    }

    // Destructor in the base class
    ~Base() {
        std::cout << "Base class destructor called" << std::endl;
    }

private:
    int baseValue;
};

// Derived class
class Derived : public Base {
public:
    // Parameterized constructor in the derived class
    Derived(int baseValue, int derivedValue) : Base(baseValue), derivedValue(derivedValue) {
        std::cout << "Derived class constructor called with value: " << derivedValue << std::endl;
    }

    // Destructor in the derived class
    ~Derived() {
        std::cout << "Derived class destructor called" << std::endl;
    }

private:
    int derivedValue;
};

int main() {
    // Creating an object of the derived class
    Derived derivedObject(10, 20);

    return 0;
}

When an object of the Derived class is created, the following sequence of events occurs:

  1. The Derived constructor is called.
  2. The Base constructor is invoked from the Derived constructor using the initialization list.
  3. The Base constructor initializes the baseValue.
  4. The Derived constructor initializes the derivedValue.
  5. The Derived destructor is called when the object goes out of scope.
  6. The Base destructor is called from the Derived destructor.

The output of the program would be:

Base class constructor called with value: 10
Derived class constructor called with value: 20
Derived class destructor called
Base class destructor called

Best Practices

Member initialization via a member initialization list

To have a constructor initialize members, we do so using member initializer list. Member initialization lists are something that is best learned by example.

#include <iostream>

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo(int x, int y)
        : m_x { x }, m_y { y } // here's our member initialization list
    {
        std::cout << "Foo(" << x << ", " << y << ") constructed\n";
    }

    void print() const
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ")\n";
    }
};

int main()
{
    Foo foo{ 6, 7 };
    foo.print();

    return 0;
}

Different syntax of initializing member variables using constructor:

  • Uniform Initialization Syntax:
Foo(int x, int y)
    : m_x { x }, m_y { y } // here's our member initialization list
{}

In this style, uniform initialization syntax (introduced in C++11) is used for member initialization. It involves using curly braces {} to initialize the member variables.

  • Initializer List:
Foo(int x, int y) : m_x(x), m_y(y) {}

In this style, the member variables are initialized using constructor's initializer list. This list appears after the colon (:). It involves using round braces () to initialize the member variables.