Concepts / Defining a Complex Number Class

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.

  • Programming

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?

  • Yes, it compiles

  • No, the compiler rejects it

Reveal answer
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.

cpp
Notes on the code
  • The const after the parameter list of real() and imag() 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.

doublereturn typerealmember function name()empty parameter listconstpromise: will not modifythe object{ return re; }body only reads re
Where does the trailing const sit in a member function declaration, and what part of the signature does it modify?

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.

Call on a complexvalueIs the object const?Call compilesIs the member markedconst?Compiler rejects callnoyesyesno
When I call a member function on a const complex number, does the compiler accept the call or reject it, and what in the function's declaration decides that?

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.

real() constreads re and returns itreal(double)writes reimag() constreads im and returns itimag(double)writes im
How do a const accessor such as real() and a non-const mutator such as set_real() differ in what they may do to the object's members? In this class the setter is named real(double).
Propertyconst accessornon-const mutator
Example in the classreal() constreal(double)
What it does to re or imOnly reads the stored valueWrites a new value into the stored part
Marked const after parameter listYesNo
Callable on a const complex valueYesNo, the call is rejected

Accessors and mutators in the complex class

Worked Example: Calls on a const Complex Value

cpp
Notes on the code
  • 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.
Worked example

Deciding whether a call compiles

Given const complex z, decide whether z.real(5.0) compiles, using the two questions from earlier.

  1. 1
    Ask the first question

    Is the object const? z is declared const, so the answer is yes, and the second question applies.

  2. 2
    Find the member function

    The call z.real(5.0) uses the setter real(double). Its declaration has no const after the parameter list.

  3. 3
    Ask the second question

    Is the member function marked const? No. A const object can call only const member functions, so the call is rejected.

  4. 4
    Locate the responsible line

    The error sits on the call site z.real(5.0). The declaration of real(double) is the reason it is rejected.

Result

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 like

    A member function marked const that writes re = r inside its body.

    Why it fails

    The const marker is a promise that the function will not modify the object, and the compiler enforces it.

    Do this instead

    Remove 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 like
    const double real() written where double real() const was intended.
    Why it fails

    The 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 instead

    Put 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 like
    z += w where z is declared const.
    Why it fails

    operator+= changes re and im, so it is not marked const, and a const object can call only const member functions.

    Do this instead

    Call 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 like

    Reading the body of real(double) when the compiler reports an error on the line z.real(5.0).

    Why it fails

    The rejected call is the line the compiler points to. The declaration explains the rejection, but the failing line is the call site.

    Do this instead

    Start at the reported line, identify the member function being called, and check its declaration for a trailing const.

Practice Predicting Compile Results

EASY

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 and imag() 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) and imag(double) are rejected on a const complex value while the getters real() const and imag() 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.

Related concepts

More concepts starting with D