Concepts / Constructors and Member Initialization

Constructors and Member Initialization

A member function marked const promises that it will not modify the object it is called on, and the compiler enforces that promise.

  • Programming

Why Some Member Functions Promise Not to Change Anything

A user-defined arithmetic type such as a complex number class is built from member functions. Some of those functions only read the object's data, while others change it. C++ lets you record that difference in the declaration itself, so that a reader of the class, and the compiler, can tell in advance what a call is allowed to do to the object it is called on.

cpp
Notes on the code
  • The two getters real() and imag() are marked const
  • The two setters real(double) and imag(double) are not marked const
  • The setter call c.real(5.0) changes the stored value re

Before reading the rules, run the program in your head and keep track of which member functions are called on the object. The setter changes re, and the getter only reads it. The rest of this article explains how the declaration records that difference and what happens when you ask the compiler to allow a change on an object that was declared not to change.

Tracing Values Through a Complex Object

Follow one complex value through two steps. The object is created with the real part 1.0 and the imaginary part 3.0. Then the real part is replaced with 5.0 through the setter. The diagram shows what each member holds after each step. The imaginary part never changes, because no line in this sequence writes to it.

c afterconstructionre = 1.0, im = 3.0c after c.real(5.0)re = 5.0, im = 3.0c.real(5.0)
After construction and then a call to the real setter, what value does each member hold, and which step changed it?
Worked example

Following the real part of one object

Starting from complex c(1.0, 3.0), what are the real and imaginary parts after the statement c.real(5.0) runs?

  1. 1
    Start from construction

    The declaration complex c(1.0, 3.0) assigns 1.0 to re and 3.0 to im, so re is 1.0 and im is 3.0.

  2. 2
    Identify the write

    The statement c.real(5.0) calls the setter real(double). Its body assigns the incoming value to re, so re is overwritten with 5.0.

  3. 3
    Check the other member

    The setter body writes only re. Nothing in the statement touches im, so im keeps the value 3.0.

Result

After the setter runs, re is 5.0 and im is 3.0.

Reading the const Promise in a Signature

The word const placed after the parameter list of a member function is a promise made to the compiler and to other programmers. It states that the function will not modify the object it is called on. In the complex class, the two getters real() and imag() carry this promise. Each one simply returns a stored value, and nothing in its body writes to re or im.

doublereturn type: the getteryields a doublerealfunction name()no parameters are passed inconstpromise: the object is notmodified
In the declaration double real() const, which part of the signature marks the function as const, and what does that position promise?

Reading the declaration double real() const tells you three things before you see the body: the function returns a double, it takes no arguments, and it leaves the object unchanged. The const marking sits at the end of the signature, after the parentheses, which is where the promise about the object is recorded. The setters real(double) and imag(double) have no such marking, so the declaration itself tells you they are allowed to change the object.

Deciding Whether a Const Call Compiles

A useful way to check whether a call is legal is to ask two questions in order. First, is the object const? Second, is the member function marked const? If the object is not const, any member function may be called on it. If the object is const, only a member function marked const may be called.

Member functioncallIs the object const?Is the functionmarked const?CompilesCompilesRejectednoyesyesno
Given a member function call, which checks decide whether the compiler accepts it?
cpp
Notes on the code
  • c.real() compiles: c is not const, and the getter is const anyway
  • c.real(5.0) compiles: c is not const
  • k.real() compiles: the getter is marked const
  • k.real(7.0) is rejected: the setter real(double) is not marked const, and k is const

What do you think happens?

Given const complex k(2.0, 4.0), does the call k.real(7.0) compile?

  • Yes, it compiles

  • No, it is rejected

Reveal answer
Answer:

No, it is rejected

The setter real(double) is not marked const, and a const object can call only const member functions. The rejected line is the call to the setter. The getter k.real() on the same object is accepted because real() const is marked const.

Operations That Stay Inside the Class

Not every operation on a complex number needs to live inside the class body. Many useful operations do not need direct access to the internal representation, so they can be defined separately from the class definition. The class itself keeps the operations that do need the stored parts.

The compound assignment operators are the example of member functions that do need that access. operator+= adds the other value's parts into re and im and then returns *this. Because it changes the object by design, it is not marked const. This is the same distinction as before: a function that must change the object goes unmarked, and a function that only reads it carries the const promise.

Constructors and Their Job

Definition

A constructor is a member function that initializes objects of a class. It is selected by the arguments supplied at declaration, and it is the mechanism that sets each stored part of the object to a starting value at the moment the object comes into existence.

Every object of a class holds data, and that data has to start somewhere. In a complex number class that stores a real part and an imaginary part, each stored part needs a value before any arithmetic can happen. The constructor is the place where that starting value is arranged, and the arrangement matters for correctness because later code assumes the parts already hold meaningful numbers.

Reading the Three Constructors

cpp
Notes on the code
  • Each constructor has the same name as the class and no return type in front of it
  • The entries differ only in their parameter lists
  • The member initializer list after the colon, such as :re{r}, im{i}, assigns the incoming values into the stored parts
  • The parameter names r and i are placeholders for the values passed in, not the stored members

Read the three constructors as three entries in the same list. This is the feature called overloading: one name, several constructors, with the compiler choosing among them by the arguments present. Constructors can be overloaded to accept different numbers and types of arguments, and the three entries above are a concrete instance of that rule. The one-argument constructor shown here is an illustration added for this article; the source itself shows the empty and two-argument forms as the complex class's constructors.

Matching an Argument List to a Constructor

The compiler selects a constructor by looking at the argument list in the declaration. An empty list selects the constructor that takes no arguments. A single double selects the one-argument form. Two doubles select the two-argument form. The declaration's parentheses, and the number and type of what is inside them, determine the match.

complex a;complex()no arguments: defaultconstructorcomplex b(2.0);complex(double r)one double argumentcomplex c(1.0,3.0);complex(double r,double i)two double argumentsempty listone argumenttwo arguments
For each declaration, which constructor is selected by its argument list?

The Default Constructor and Its Guarantee

Definition

A default constructor is a constructor that can be invoked without an argument. In the complex class, that role belongs to complex(). Being the default does not depend on where the constructor sits in the class. A constructor can be the default one even though it appears among other constructors that take arguments; what makes complex() the default is simply that a caller can write the declaration with nothing supplied.

The source makes a specific claim about the default constructor: by defining one, you eliminate the possibility of uninitialized variables of that type. The benefit is a guarantee about the class as a whole. Any variable of type complex that is declared without arguments starts with a defined value of {0,0}, so code that reads its real and imaginary parts does not depend on luck.

What do you think happens?

If you declare complex a; with no arguments, what values do its real and imaginary parts hold?

Reveal answer
Answer:

Both parts are 0

The default constructor complex() starts every argument-free declaration at {0,0}, so re and im both hold 0.

Tracing an Unexpected Value to Its Constructor

When a value from a constructed object is not what you expected, the fastest route is to trace which constructor ran and what it assigned. Work this case the way a debugger would, one decision at a time. Suppose a colleague wrote the one-argument constructor with a mistake, so that its initializer list reads :re{r}, im{r}, and then a program declares complex b(2.0) and prints the imaginary part.

Worked example

Locating the source of an unexpected imaginary part

After complex b(2.0) is declared, the imaginary part is 2.0 instead of the expected 0. Which line wrote that value?

  1. 1
    Match the declaration

    complex b(2.0) has one argument, so the compiler selects the one-argument constructor complex(double r).

  2. 2
    Read the initializer list

    The initializer list is :re{r}, im{r}. The real part is set from r, which is 2.0, which is correct.

  3. 3
    Find the wrong write

    The imaginary part is also set from r rather than from the value 0. That initializer is the line that wrote 2.0 into im.

  4. 4
    Correct the line

    Changing the initializer to :re{r}, im{0} makes the one-argument constructor store 0 in im, which matches the intended behavior.

Result

The unexpected value came from the initializer for im in the one-argument constructor, not from the declaration or from the getter.

Mistakes Beginners Make with Constructors and const

  • Common mistake

    Assuming a const object can call any member function

    What it looks like
    const complex k(2.0, 4.0); k.real(7.0);
    Why it fails

    The setter real(double) is not marked const, and a const object may call only const member functions.

    Do this instead

    Call only the getters real() and imag() on a const object, or remove const from the declaration if the object must change.

  • Common mistake

    Reading a const marking as a claim about the variable name only

    Why it fails

    The promise concerns the object the function is called on, so the check is made at each call site.

    Do this instead

    Ask whether the object is const and whether the called function is marked const, in that order.

  • Common mistake

    Expecting an argument-free declaration to hold garbage values

    What it looks like

    complex a;

    Why it fails

    Defining a default constructor guarantees a defined starting value for every argument-free declaration, namely {0,0}.

    Do this instead

    Rely on the default constructor's guarantee, and trace an unexpected value back to the constructor that actually ran.

  • Common mistake

    Forgetting that the parameter names in a constructor are not the stored members

  • Common mistake

    Marking an operation that changes the object, such as operator+=, as const

Practice Tracing Constructors and const

EASY

Given the complex class from this article, declare const complex m(3.0, 6.0). Decide which of the lines m.real(), m.imag(), and m.real(1.5) compile, and explain each decision using the declaration of the function that is called.

Hints
  • Ask first whether the object is const, then whether the called member function is marked const.
  • m.real() calls the getter real() const, which carries the promise.
  • m.real(1.5) calls the setter real(double), which is not marked const.

Key Takeaways

  • A member function marked const promises that it will not modify the object it is called on, and the compiler enforces that promise at each call.
  • A call on a const object compiles only when the called member function is marked const, so the getters real() const and imag() const are accepted and the setters are rejected.
  • A constructor is a member function that initializes an object of a class, and the compiler selects it by matching the argument list in the declaration.
  • The default constructor is the one callable without arguments, and defining it guarantees that an argument-free declaration starts at {0,0}.
  • When a value is unexpected, trace which constructor ran and which initializer wrote the value, because the initializer list is where the stored parts are assigned.

Related concepts

More concepts starting with C