Defining a Complex Number Class
Read the class first, then the code that uses it, and keep track of which member functions are called on the object.
When a Value Must Stay Unchanged
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 the compiler can check how each function is used. This article follows one complex number class from start to finish and asks, for each call, whether the compiler accepts it and what in the declaration decides the answer.
What do you think happens?
Given const complex z(3.0, 4.0); and a non-const setter real(double) in the class, does the call z.real(5.0) compile?
Reveal answer
No, the compiler rejects it.
A const object can call only const member functions. The setter real(double) is not marked const, so the call is rejected on that line. The getter real() const is marked const and would be allowed.
Tracing the Members of the Complex Class
Read the class first, then the code that uses it, and keep track of which member functions are called on the object. The class stores two values, re and im, which hold the real and imaginary parts. Four member functions touch them. The getters real() const and imag() const return the stored values. The setters real(double) and imag(double) overwrite them. The compound assignment operator operator+= adds another value's parts into re and im and then returns the object itself.
- The const after the parameter list of
real()andimag()is the promise that these functions will not modify the object. - The setters and operator+= have no trailing const because they change re or im.
- The source excerpt does not show the constructor or the setter return types. The void setters here are one way to write them for illustration.
Before any call is made, you can already sort the members into two groups by reading the declarations. The getters are the members that carry the const promise, because each one returns a stored value and nothing in its body writes to re or im. The setters and operator+= do not carry the promise, because they exist to change the object.
Reading the const Marker 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. The marker does not belong to the return value or to the parameters. It belongs to the function, and it describes what the function may do to the object that is the target of the call.
Two Questions for Checking a Call
A useful way to check whether a call is legal is to ask two questions in order. First, is the object const? If it is not, the call is allowed, because a non-const object has no restriction on its member functions. Second, is the member function marked const? If the object is const, the call is allowed only when the member function carries the const marking. Ask these two questions for every call before you run the program.
Follow the diagram for a const complex value. The first question leads to the second, and only a const-marked member function passes it. The getters real() const and imag() const pass. The setters real(double) and imag(double) do not, which is why a const complex value cannot use them.
Accessors and Mutators Side by Side
The two kinds of member function differ in what they are allowed to do and in what they require of the object they are called on. Accessors read a stored value and are marked const. Mutators change a stored value and are left unmarked. The table and diagram below set the real part against the members that handle it.
| Property | const accessor | non-const mutator |
|---|---|---|
| Example in the class | real() const | real(double) |
| What it does to re or im | Only reads the stored value | Writes a new value into the stored part |
| Marked const after parameter list | Yes | No |
| Callable on a const complex value | Yes | No, the call is rejected |
Accessors and mutators in the complex class
Worked Example: Calls on a const Complex Value
- Only the two const-marked getters can be called on z.
- The two rejected lines are the call sites the compiler points to, because the declarations they use are not marked const.
- The same setter and operator+= calls are allowed on m, because m is not const.
Deciding whether a call compiles
Given const complex z, decide whether z.real(5.0) compiles, using the two questions from earlier.
- 1Ask the first question
Is the object const? z is declared const, so the answer is yes, and the second question applies.
- 2Find the member function
The call
z.real(5.0)uses the setterreal(double). Its declaration has no const after the parameter list. - 3Ask the second question
Is the member function marked const? No. A const object can call only const member functions, so the call is rejected.
- 4Locate the responsible line
The error sits on the call site
z.real(5.0). The declaration ofreal(double)is the reason it is rejected.
z.real(5.0) does not compile. The getter z.real() does compile, because real() const carries the const promise.
Operations That Need Internal Access
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 re and im. The compound assignment operator operator+= is an example of a member function that needs that access. It adds the other value's parts into re and im and then returns the object itself. Because it changes the object by design, it is not marked const.
Mistakes When Reading const Declarations
Common mistake Assuming a const-marked function may still change the object
What it looks likeA member function marked const that writes re = r inside its body.
Why it failsThe const marker is a promise that the function will not modify the object, and the compiler enforces it.
Do this insteadRemove the write, or remove the const marking if the function really must change the object.
Common mistake Placing const before the return type instead of after the parameter list
What it looks likeconst double real() written where double real() const was intended.Why it failsThe const in front of the return type qualifies the returned value. It does not promise anything about the object the function is called on.
Do this insteadPut const after the closing parenthesis of the parameter list to mark the member function.
Common mistake Expecting operator+= to be callable on a const object
What it looks likez += w where z is declared const.Why it failsoperator+= changes re and im, so it is not marked const, and a const object can call only const member functions.
Do this insteadCall operator+= on a non-const object, or use a copy if the value must be combined without changing the original.
Common mistake Searching for the error inside the class instead of at the call
What it looks likeReading the body of real(double) when the compiler reports an error on the line z.real(5.0).
Why it failsThe rejected call is the line the compiler points to. The declaration explains the rejection, but the failing line is the call site.
Do this insteadStart at the reported line, identify the member function being called, and check its declaration for a trailing const.
Practice Predicting Compile Results
Assume the complex class from this article, with const complex z(3.0, 4.0); declared earlier. For each call below, predict whether it compiles, and name the member function whose declaration decides the answer: (a) z.imag(); (b) z.imag(1.0); (c) m.imag(1.0) where m is a non-const complex object; (d) z += m.
Hints
- Ask the two questions in order: is the object const, and is the member function marked const?
- Only the getters
real()const andimag()const carry the const promise in this class. - A non-const object has no restriction on which member functions it may call.
Key Takeaways
- A member function marked const after its parameter list promises that it will not modify the object it is called on, and the compiler enforces that promise.
- A const object can call only const member functions, so the setters
real(double)andimag(double)are rejected on a const complex value while the gettersreal()const andimag()const are allowed. - To check a call, ask first whether the object is const and second whether the member function is marked const; the rejected line is the call site, and the declaration explains why.
- Operations that must change the object, such as operator+=, stay unmarked, and operations that need no access to the internal representation can be defined outside the class.