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.
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.
- The two getters
real()andimag()are marked const - The two setters
real(double)andimag(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.
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?
- 1Start 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.
- 2Identify the write
The statement
c.real(5.0)calls the setterreal(double). Its body assigns the incoming value to re, so re is overwritten with 5.0. - 3Check the other member
The setter body writes only re. Nothing in the statement touches im, so im keeps the value 3.0.
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.
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.
c.real()compiles: c is not const, and the getter is const anywayc.real(5.0)compiles: c is not constk.real()compiles: the getter is marked constk.real(7.0)is rejected: the setterreal(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?
Reveal 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
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
- 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.
The Default Constructor and Its Guarantee
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
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.
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?
- 1Match the declaration
complex b(2.0) has one argument, so the compiler selects the one-argument constructor
complex(double r). - 2Read 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.
- 3Find 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.
- 4Correct the line
Changing the initializer to :re{r}, im{0} makes the one-argument constructor store 0 in im, which matches the intended behavior.
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 likeconst complex k(2.0, 4.0); k.real(7.0);Why it failsThe setter
real(double)is not marked const, and a const object may call only const member functions.Do this insteadCall 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 failsThe promise concerns the object the function is called on, so the check is made at each call site.
Do this insteadAsk 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 likecomplex a;
Why it failsDefining a default constructor guarantees a defined starting value for every argument-free declaration, namely {0,0}.
Do this insteadRely 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
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 getterreal()const, which carries the promise.m.real(1.5)calls the setterreal(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 andimag()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.