It’s a complex topic ( complex only if actually know about the ins and outs of it ) and has a lots of nuances and gotchas to it in C++ so here’s my breakdown.

A constructor is a special mapping function that takes in uninitialized memory and possible some parameters to initialize memory to a “valid” state for the class. Some contructor is “always” called when an object is created.

Default Constructor

The simplest case:

#include <iostream>
 
class C {
    int x;
};
 
int main() {
    C obj;
    std::cout << sizeof(obj) << std::endl;
}

Here’s what’s the class actually looks like from cppinsights.io:

class C
{
  public:
    inline constexpr C() noexcept = default;
};

This is the default constructor that the compiler generates automatically. Always takes no parameters.

What ist the = default here and difference with C() {}?

= default intializes members with “default” values for each member whereas C(){} would do nothing of that sort. You could call the later a no argument constructor.

This distinction matters in the case of value initialization. Refer Initializations.

Another problem would be the loss of trivial type properties.

#include <iostream>
#include <type_traits>
 
class DefaultVersion {
   public:
    DefaultVersion() = default;
};
 
class EmptyVersion {
   public:
    EmptyVersion() {};
};
 
int main() {
    DefaultVersion d;
    EmptyVersion e;
 
    std::cout << "DefaultVersion is trivially default constructible: "
              << std::is_trivially_default_constructible<DefaultVersion>::value << std::endl;
    std::cout << "EmptyVersion is trivially default constructible: "
              << std::is_trivially_default_constructible<EmptyVersion>::value << std::endl;
 
    std::cout << "DefaultVersion is trivial: "
              << std::is_trivial<DefaultVersion>::value << std::endl;
    std::cout << "EmptyVersion is trivial: "
              << std::is_trivial<EmptyVersion>::value << std::endl;
 
    return 0;
}

Trivial types can be:

  • copied with memcpy
  • passed to functions by value without invoking the constructor

Non-trivial types include:

  • Individual constructor calls
  • Standard copy/move operations
  • No memcpy optimizations

When is the default constructor not generated?

  • if “any” constructor is defined
  • if the members are not default constructible
  • if the class has a base class that is not default constructible

What is the inline here?

This is no different that inlining a function. The creationc is inlined in this case.

What is the constexpr here?

Allows creation of objects at compile time. It must use literal types aka a type that can be used in constant expressions.

Why noexcept?

This marks the constructor as not throwing exceptions. Why this matter is it allows the compiler to skip exception handling pathways and generate more optimized code. Also, stack unwinding is not needed if marked as noexcept.

The tradeoff is that if an exception is throws, it’s an immediate std::terminate call with no guarantee of cleanup or destructors being called.

Any constexpr constructor or function is evaluated at compile time and always inlined.

std::initializer_list and preference in brace initialization

std::initializer_list<T> is a lightweight proxy object that provides access to an array of objects of type const T (that may be allocated in read-only memory).

This is automatically constructed when you use the brace initialization syntax {}.

Note that when an std::initializer_list the compiler always prefers the constructor that takes an std::initializer_list over other constructors.

class C {
    std::vector<int> elems;
 
   public:
    C(std::initializer_list<int> il) : elems(il) {
        std::cout << "C constructor called with initializer list." << std::endl;
    };
};
 

Can be initialized with C c{1, 2, 3};

Parameterized Constructor

Basically self-explanatory. A few things to note:

  • you almost always want to use member intializer lists to initialize members
  • an std::initializer_list constructor is preferred over a similar constructor with parameters

Copy Constructor

To initialize a new object as a copy of an existing one.

If you have a potential copy operation, the compiler generates a default copy constructor that does a member-wise copy of the object. Note that this is a shallow copy.

A default copy contuctor looks like this:

inline constexpr C(const C &) noexcept = default;

Problem with shallow copy? If it holds a resource like a pointer, the pointer is copied and both objects will point to the same memory location. This can lead to double deletion issues when both objects are destroyed ( delete on same memory twice is undefined behavior ). This also leads to dangling pointers if one object is destroyed and the other tries to access the memory.

The Rule of Three/Five

If the class holds resources, it definitely needs a destructor to free them. Rule of Three states if you define one of destructor, copy constructor, or copy assignment operator, you should define all three. In modern C++, this is often replaced by the Rule of Five, which includes move semantics ( so add move constructor and move assignment operator ).

Or rule of zero, which means define none.

It should almost always be 0/3/5 though and any other case is probably wrong.

Copy Assignment Operator

Why do you even need this? Isn’t a copy constructor enough? No, if I want to intialize from an existing object I can use the copy constructor, but what if later down the line I want assing another object to this one? The copy assignment operator does that.

For cases where memory is allocated, the copy assignment operator must delete exsting one and then copy the new one. A nice way to do this is to use the copy-and-swap idiom. Though note that you are leaving some performance since you might not need to free and reallocate memory if you can just overwrite.

C& operator=(const C& other) {
    if (this != &other) {
        delete data;
        data = new int(*other.data);
    }
    return *this;
}

Move Constructor

A move constructor is used to transfer ownership of resources from one object to another without copying the data. This is an optimization over a copy.

C(C&& other) noexcept : data(other.data) {
    other.data = nullptr;
}

Above is a very trivial example. Move semantics introduced in C++11 allow for handling of more advanced & complex types.

Move Assignment Operator

Same idea as the copy assignment operator, but for move semantics.

C& operator=(C&& other) noexcept {
    if (this != &other) {
        delete data;
        data = other.data;
        other.data = nullptr;
    }
    return *this;
}

When is what generated?

  • Copy constructor:
    • if not explcitly deleted ( obvious )
    • if no explicit move constructor or move assignment operator
    • a copy assignment operator does not affect in any way
    • a destructor does not affect as well
class C {
   public:
    C() = default;
    // C(C &) = default;
    C &operator=(C &) = default;
    // C(C &&) = default;
    // C &operator=(C &&) = default;
 
    ~C() = default;
};
 
int main() {
    C obj1;
    C obj2 = obj1;  // copy constructor invoked
 
    return 0;
}

Uncommenting any one of the move constructors or move assignment operators will disable the copy constructor. Note that the copy assignment operator is fine.

  • Copy assignment operator: follows same rules as copy contructor

  • Move contructor

    • Any of copy or copy assignment operator defined will not generate a move constructor
    • Having a destructor does not generate a move constructor
    • A move assignment operator does not generate a move constructor
#include <utility>
 
class C {
   public:
    C() = default;
    //C(C &) = default;
    C &operator=(C &) = default;
    // C(C &&) = default;
    //C &operator=(C &&) = default;
 
    //~C() = default;
};
 
int main() {
    C obj1;
    C obj2 = std::move(obj1);  // Move constructor
 
    return 0;
}

This does not generate a move constructor but it still works becuase the compiler uses the copy constructor as a fallback. Generated code from cppinsights.io:

#include <utility>
 
class C
{
 
  public:
  inline constexpr C() noexcept = default;
  inline constexpr C & operator=(C &) /* noexcept */ = default;
  // inline constexpr C(const C &) noexcept = default;
};
 
 
int main()
{
  C obj1;
  C obj2 = C(static_cast<const C &&>(std::move(obj1)));
  return 0;
}

Since a const C& can bind to a C&&, this will work. However if you uncomment the copy constructor ( and note that it’s not a cosnt C& ) this will fail.

  • Move assignment operator: same as the move constructor case. This too fallback to a copy assignment operator generation.

Note that for copy args are const but not for move. This is because move semantics are about transferring ownership, so the object being moved from can be modified (e.g., setting pointers to nullptr).