Skip to content
academia.sh

Lesson 03 / 14

Constructors and Destructors

Measures that an argument implicitly built from an int is an unnamed, temporary object, and measures its lifetime with a constructor/destructor pair.

Published Updated

In this lesson

  • Measures that an argument built through an implicit type conversion is a temporary object
  • Applies how the explicit keyword prevents implicit construction
  • Shows the order of the member initializer list and the constructor chain
Contents

The previous lesson measured which object a member function works with, through this — but that object always already existed. This lesson asks the same question at the moment an object comes to exist: when is a constructor (a member function carrying the same name as the class, run once while the object is being built) called, and when does a destructor (a member function prefixed with ~, run once while the object is being destroyed) run? This time the measurement kind is temporary: it will track the lifetime of an unnamed object, implicitly built from an argument of a function call.

Constructor and Destructor

A constructor carries the same name as the class, has no return type (not even void is written), and is called by the compiler while an object is being built. A destructor takes the form ~ClassName(), takes no arguments, and is called by the compiler when an object reaches the end of its lifetime — the programmer never calls it by hand. If a class writes no constructor at all, the compiler generates a default constructor (one taking no arguments, building each data member with its own default value); once a constructor is written, this generation stops.

A class can have more than one constructor — as long as their parameter count or type differs, they are distinguished by the same overload rules as any function. A class can, by contrast, have only one destructor; because a destructor takes no arguments, it cannot be overloaded.

The biggest difference between a constructor and an ordinary member function is that, while the constructor is running, the object is not yet complete. While Quantity(int value) runs, value_ is built by the member initializer list’s value_(value); once the body begins running, value_ already holds a valid value and can be read inside the body, but until the body runs, the object’s other members (this class has only value_, but in a class with more members, the members that follow) may not yet be built. For this reason, calling another member function through this inside a constructor’s body — especially a virtual member function, a topic outside this course’s scope — carries the risk of reaching a not-yet-fully-built state of the object; this lesson makes no such call.

The destructor’s position is symmetric: while the destructor is running, the object is still complete and valid, but once the destructor returns the object is considered gone — the memory for its member variables may still physically remain, but the program accessing it is undefined behavior (a rule this course inherits from M11/K02: undefined behavior is never run, it is only mentioned in prose). Quantity::~Quantity‘s body reads and prints value_ one last time; no code after that line should access the same object’s value_.

// quantity.hpp — a constructor, a destructor, and a constructor chain
#ifndef QUANTITY_HPP
#define QUANTITY_HPP

#include <cstdio>

class Quantity {
public:
    Quantity(int value) : value_(value) {
        std::printf("constructor ran value=%d\n", value_);
    }

    ~Quantity() {
        std::printf("destructor ran value=%d\n", value_);
    }

    int value() const { return value_; }

private:
    int value_;
};

#endif

CR5. Quantity(int value) takes an argument, and : value_(value), which runs before its body, is a member initializer list — it builds the data member value_ with the value value before the body runs. Writing value_ = value inside the body would give the same result here too, but the initializer list separates construction from assignment: by the time the body runs, value_ already exists, it is only being modified.

Implicit Construction and explicit

Quantity(int value) is a constructor that takes exactly one int argument; C++ also uses a constructor like this for implicit type conversion. If a function like show(Quantity q) is called as show(5), the compiler implicitly calls this constructor to convert 5 into a Quantity — the constructed object has no name, no variable is visible in main, but the object genuinely exists and its constructor genuinely runs.

// temporary-measurement.cpp — is a quantity implicitly built from an int a temporary
#include <cstdio>

#include "quantity.hpp"

static void show(Quantity q) {
    std::printf("inside show value=%d\n", q.value());
}

int main() {
    std::printf("before call\n");
    show(5);
    std::printf("after call\n");
    return 0;
}
before call
constructor ran value=5
inside show value=5
destructor ran value=5
after call

CR6. In the call show(5), 5 is first converted into a Quantity object through Quantity(5) — this object has no name, and is therefore temporary. This temporary is copied into show‘s parameter q (or built directly in its place — a compiler optimization outside this course’s scope); when show returns, q‘s destructor is called, and the output order shows this: the constructor runs right after the call, the destructor runs right after show returns, but before the after call line — the temporary’s lifetime is bounded by the expression show(5); alone, it does not spill into the rest of main.

This implicit conversion is not always wanted: when show(5) is written, it is not obvious to a reader whether 5 is meant to pass as a Quantity or is an integer passed by mistake. The explicit keyword prevents this: had explicit Quantity(int value) been written, show(5) would not compile, and only an explicit construction such as show(Quantity(5)) or show(Quantity{5}) would compile. This lesson leaves Quantity without explicit because what it wants to measure is exactly the temporary that implicit construction produces; but a class author should consider marking every single-argument constructor explicit — the constructors written in this course’s later lessons are also written as explicit unless stated otherwise.

The risk of implicit conversion is not only readability. When a chain of two arguments allows implicit construction — for example, if Quantity implicitly converted from another class’s constructor as well — the compiler could compile a call through an unexpected chain; the C++ standard limits this by allowing at most one implicit user-defined conversion per call, but even a single-step implicit conversion can hide a constructor call the reader did not expect. In the show(5) example this risk is small, because Quantity has only one constructor and the intent is clear; in a large class hierarchy, the same implicit construction can silently add the cost of building an object (a constructor running, a destructor running) that the caller did not notice. explicit removes this silence: every construction is then forced to appear in the source code as a visible Type(...) call.

Constructor Chains and Reverse-Order Destruction

In a class with more than one data member, the member initializer list ensures that members are built in declaration order — not the order written in the list, the order of declaration in the class body governs. Destruction happens in exactly the opposite order: the member built last is destroyed first.

// pair.cpp — the construction and destruction order of more than one member
#include <cstdio>

#include "quantity.hpp"

class Pair {
public:
    Pair() : first_(1), second_(2) {
        std::printf("pair constructor ran\n");
    }

private:
    Quantity first_;
    Quantity second_;
};

int main() {
    std::printf("block started\n");
    {
        Pair p;
        std::printf("pair constructed\n");
    }
    std::printf("block ended\n");
    return 0;
}
block started
constructor ran value=1
constructor ran value=2
pair constructor ran
pair constructed
destructor ran value=2
destructor ran value=1
block ended

CR7. Before Pair‘s own constructor body runs, first_ and second_ are built in the class body’s declaration order (not the order in the member initializer list — here the two coincide, so no difference is visible, but the order is always determined by declaration); each build triggers its own Quantity constructor, which is why two separate constructor ran lines appear before the pair constructor ran line. When p is destroyed as the inner block closes, Pair’s own destructor (generated by the compiler because none is written here, printing nothing) runs first, and then its members are destroyed in reverse order: second_ first, first_ after. Construction and destruction mirror each other.

In M10/K02 (Rust), a value going out of scope ran Drop; C++’s destructor answers the same idea, but the triggering rules differ, and this lesson does not deepen that comparison.

Declaration order being binding means the member initializer list’s written order is ignored — some compilers report as a warning when the list’s order does not match the declaration order, because the two orders differing can mislead a reader: even if the list were written as second_(2), first_(1), construction order would still put first_ first, because first_ is declared first in the class body. Pair writes the list in the same order as the declaration for this lesson, so the two orders coincide and no confusion arises — but the rule is always declaration order, never the order written in the list.

Reverse-order destruction has another consequence: a member’s constructor can safely use a resource built by an earlier member — while second_ is being built, first_ is already fully built, because it precedes it in declaration order. The same guarantee runs in reverse during destruction: while second_ is being destroyed, first_ has not yet been destroyed, so second_‘s destructor can, if needed, still safely access first_. This symmetry is the foundation the next lesson’s subject, RAII (Resource Acquisition Is Initialization), is built on: a member holding a resource is not destroyed before the object holding it goes out of scope.

Constructors Calling Each Other

When a class has more than one constructor, one constructor can call another by delegating to it, to avoid duplicating code between them: writing Quantity() : Quantity(0) {} defines a constructor that, when called with no arguments, delegates its work to the single-argument constructor. Delegation is written by calling the class’s own name in the member initializer list — here Quantity(0) calls a constructor of the class itself, not of another member. The delegated-to constructor runs completely (including its own body), and the delegating constructor’s body runs after it. Quantity does not use delegation within this lesson’s scope — it has only one constructor — but this technique is the standard way to keep shared construction logic between multiple constructors in a single place; it must not be confused with building a member in the initializer list, since one calls the class’s own name and the other calls a member’s name.

The Default Constructor and Default Values

Quantity does not write a no-argument constructor, because it wants a quantity to always be built with a starting value — writing Quantity t; does not compile for this class, because its one constructor requires an int. If a class writes no constructor at all, the compiler generates a no-argument default constructor; this generated constructor builds every data member with its own default value — members of class type run their own default constructors, while members of primitive types such as int (unless an initializer is given in the member declaration) are left uninitialized, meaning they carry a random memory value. This is a behavior inherited from C and unchanged in C++: a class like Pair that only has members of class type has a safe default constructor, because every member runs its own constructor; but a member such as int amount_; left without an initializer is left uninitialized by the default constructor. The classes written in this course give every member of a primitive type an explicit starting value, either in the declaration (int amount_ = 0;) or in the member initializer list; reading an uninitialized member is also undefined behavior and is not run.

The Bounding Measurement

The expression this lesson measures — the implicitly built Quantity inside show(5) — is of kind temporary: it has no name, and it ceases to exist at the end of the expression show(5);. But a temporary’s lifetime does not always stay this short: a temporary, if bound to a const reference, has its lifetime extended to match the reference’s. Had const Quantity& r = Quantity(7); been written, the temporary on the right would normally cease to exist at the end of the expression, but because it is bound to r, its lifetime is extended to the end of r‘s scope — the compiler does this automatically, the programmer writes nothing extra. This extension only applies to const references (and to rvalue references, outside this course’s scope); writing Quantity& r = Quantity(7); does not compile, because a non-const reference cannot bind to a temporary. That the reference itself cannot be rebound to another temporary afterward — that a reference, once built, always refers to the same object — is what the references lesson in the next topic measures.

This boundary explains why temporaries must be used carefully in chains of function calls: in a call such as show(Quantity(5)), the temporary lives only for the duration of the expression in which show runs; if a pointer to the temporary, or a non-const reference to it, is carried out of that expression, that pointer or reference becomes dangling once the expression ends — the memory it points at is no longer valid. That Quantity’s value() function returns an int, not a const Quantity& or a Quantity*, exists exactly to avoid this risk: an int is copied as a value, and even if the temporary itself ceases to exist, the copied value remains valid.

These three rules together — that implicit construction produces a temporary, that declaration order governs construction and destruction, and that a const reference can extend a temporary’s lifetime — show that an object “existing” and an object “being named” are not the same thing. The Quantity inside show(5) is built without ever being assigned to a variable, its constructor runs, it is used, its destructor runs — and throughout this entire process no name ever points at it. The next lesson’s RAII uses exactly this observation: releasing a resource does not require an object to be named, only that its lifetime reach its end — the compiler always knows that moment, whether the object has a name or not.

Summary

  • The expression this lesson measures is of kind temporary: the Quantity implicitly built inside show(5) has no name and ceases to exist at the end of the expression show(5);.
  • A constructor carries the same name as the class; a destructor is written with a ~ prefix; a destructor takes no arguments and cannot be overloaded.
  • A single-argument constructor allows implicit type conversion; explicit prevents this and allows only explicit construction.
  • The member initializer list builds members in declaration order; destruction happens in the reverse order.
  • A temporary bound to a const reference has its lifetime extended to match the reference’s scope.

Next Step

This lesson measured when an object comes to exist and ceases to exist; the next lesson uses that knowledge to safely release a resource (such as memory or a file handle) — measuring how a destructor releases a member, by separating a guard from the member inside it.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close