Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 3 of 12

A constructor runs when an object is created. Its job is to leave the object in a valid state, so that no other member function ever has to wonder.

#include <iostream>
#include <string>

class Person {
    std::string name;
    int age;

public:
    Person(const std::string& n, int a) : name(n), age(a) {}

    void print() const {
        std::cout << name << " is " << age << "\n";
    }
};

int main() {
    Person p("Sokha", 22);
    p.print();
    return 0;
}
Sokha is 22

A constructor has the class's name and no return type. It is not called; it runs automatically when an object is created.

The member initializer list

That : name(n), age(a) between the parameters and the body is the member initializer list, and it is not a stylistic alternative to assignment in the body.

#include <iostream>
#include <string>

class Widget {
    std::string label;

public:
    // Initialisation: label is constructed directly from `text`.
    Widget(const std::string& text) : label(text) {}

    // Assignment: label is first default-constructed, then overwritten.
    // Two operations where one would do.
    // Widget(const std::string& text) { label = text; }

    const std::string& get() const { return label; }
};

int main() {
    Widget w("hello");
    std::cout << w.get() << "\n";
    return 0;
}
hello

Members are constructed before the constructor body runs. So assigning in the body means the member is built twice: default-constructed, then assigned over. For an int that is free; for a std::string or std::vector it is a wasted allocation.

Three cases where the body will not work at all

For these, the initializer list is the only option:

const members cannot be assigned:

class Config {
    const int max_retries;

public:
    Config(int n) : max_retries(n) {}    // only way
    // Config(int n) { max_retries = n; }   error: assignment of read-only member
};

Reference members must be bound at construction (lesson 7.3):

class Logger {
    std::ostream& out;

public:
    Logger(std::ostream& o) : out(o) {}  // only way
};

Members with no default constructor have nothing to default-construct:

class Engine {
public:
    Engine(int power) { (void)power; }   // no Engine() exists
};

class Car {
    Engine engine;

public:
    Car() : engine(150) {}               // only way
};

Initialisation order is declaration order

This one causes real bugs:

#include <iostream>

class Confusing {
    int a;
    int b;

public:
    // Written b first, but members initialise in DECLARATION order:
    // a is initialised before b, using b's value — which is garbage.
    Confusing(int v) : b(v), a(b) {}

    void print() const { std::cout << "a=" << a << " b=" << b << "\n"; }
};

int main() {
    Confusing c(5);
    c.print();       // b is 5; a read b before b existed
    return 0;
}

Members initialise in the order they are declared in the class, not the order written in the list. a is declared first, so a(b) runs while b is still uninitialised.

Compile that with -Wall and GCC tells you:

warning: 'Confusing::b' will be initialized after [-Wreorder] warning: 'int Confusing::a' [-Wreorder]

Keep the list in declaration order and the warning never appears. Better still, do not initialise one member from another.

Multiple constructors

Constructors overload like any function (lesson 5.6):

#include <iostream>
#include <string>

class Rect {
    double w;
    double h;

public:
    Rect() : w(1.0), h(1.0) {}                    // default
    Rect(double side) : w(side), h(side) {}       // square
    Rect(double width, double height) : w(width), h(height) {}

    double area() const { return w * h; }
};

int main() {
    Rect a;
    Rect b(3.0);
    Rect c(2.0, 5.0);

    std::cout << a.area() << " " << b.area() << " " << c.area() << "\n";
    return 0;
}
1 9 10

The default constructor, and when you lose it

If you write no constructors, the compiler generates a default one. Write any constructor and that free default disappears:

#include <iostream>

class NeedsArgs {
    int value;

public:
    NeedsArgs(int v) : value(v) {}       // writing this removes the default
    int get() const { return value; }
};

int main() {
    NeedsArgs a(5);
    // NeedsArgs b;      error: no matching function for call to 'NeedsArgs::NeedsArgs()'

    std::cout << a.get() << "\n";
    return 0;
}
5

Lesson 9.12 shows how to ask for the default back with = default.

explicit: stopping surprise conversions

A constructor taking one argument doubles as an implicit conversion, which is rarely what you want:

#include <iostream>

class Meters {
    double value;

public:
    explicit Meters(double v) : value(v) {}
    double get() const { return value; }
};

void travel(const Meters& m) {
    std::cout << "travelling " << m.get() << "m\n";
}

int main() {
    travel(Meters(100.0));    // explicit and clear

    // travel(100.0);         // error, thanks to `explicit`.
    //                        // Without it this compiles, and 100.0 silently
    //                        // becomes Meters — even if it was seconds.
    return 0;
}
travelling 100m

Mark single-argument constructors explicit unless you specifically want the conversion. The cost of being wrong in the other direction is a program that compiles while meaning something you did not write.

Try it

Write a class Fraction with a numerator and denominator, a constructor that rejects a zero denominator, and a value() returning the division. Add a second constructor for whole numbers. Make sure both initialise via the list.

Checkpoint

  • A constructor runs on creation and leaves the object valid.
  • The member initializer list initialises members; assigning in the body constructs twice.
  • const members, reference members, and members without a default constructor require the list.
  • Members initialise in declaration order, not list order — -Wreorder warns.
  • Writing any constructor removes the compiler-generated default one.
  • Mark single-argument constructors explicit to prevent silent conversions.
  • A constructor can delegate to another by naming it in the initializer list.

Questions about this lesson

Log in to ask a question.