Concepts / Const Correctness in C++ Classes

Const Correctness in C++ Classes

A constructor is a special member function used for object initialization, so it is the place to make sure each new object starts in a usable state.

  • Programming

Why a New Object Needs a Known Starting State

Imagine you are handed a new object and asked to call one of its member functions. The result of that call can only be sensible if the object is already in a known, usable condition. An object that has been created but never set up is in an unpredictable state, and every function that reads its data inherits that unpredictability. C++ classes deal with this moment directly, and the same design ideas also govern which functions belong inside a class and which promise not to change an object at all. Together these ideas are what the course calls const correctness in classes.

Before reading on, try this question for yourself: if a class has a function that returns the current value stored in an object, and nothing ever sets that value, what should the function return? Hold on to your answer. The rest of this article shows how the class design can make that question impossible to get wrong.

The Life of an Object Before Its Constructor Runs

Every object is created from a class and then used by the program. The source describes an object's creation as a sequence of events. First, some code asks for a new object. Second, the class's constructor takes responsibility for initialization. Third, the object that comes out of that step is in its starting condition rather than an arbitrary one. Only after this third stage can the rest of the program safely call the object's member functions.

Code requestsobjecta new object is wantedConstructor runsinitialization workUsable objectstarting condition is knowncreationinitialized
What state does an object have before its constructor runs, what does the constructor change, and when does the object become usable?

The diagram shows the order of interactions: the code that requests the object, the class's constructor doing the initialization work, and the resulting object that the rest of the program can then use. The important point is that the constructor sits between the request and the usable object. If initialization were handled ad hoc by whatever code happened to touch the object first, different parts of the program could see the object in different conditions.

Writing a Constructor That Sets the Starting State

Definition

A constructor is a special member function used for object initialization. Its job is to make sure each new object starts in a usable state.

The practical design rule from the source is to make initialization the constructor's responsibility. When that work is placed in one dedicated member function, the rest of the class can rely on objects arriving in a consistent state. Without this rule, each function in the class would have to check whether setup had already happened before doing its own work. The constructor removes that burden from every other function.

cpp
Walk through it
  1. 1
    The class declares a constructor named Counter, which has no return type, and its member initializer list sets count_ to 0 before the body runs.
  2. 2
    The public functions value and increment are the interface that the rest of the program uses.
  3. 3
    In main, the statement Counter c creates an object, and the constructor runs as part of that creation.
  4. 4
    Calling c.value() is safe because the object was initialized first, so the function returns 0.
Notes on the code
  • The constructor Counter() is the single place where the starting state is decided
  • count_ is set to 0 before any other function can read it
  • value() reads the stored count and does not change it
  • increment() changes the stored count and is therefore not const
  • main creates a Counter, so the constructor has already run before value() is called
Worked example

Tracing the state of a Counter from creation to use

Starting from the Counter class above, what is the state of the object at each step of main, and what does value return?

  1. 1
    Before the constructor

    The object is not yet usable. Its initialization is the constructor's job, and no member function has been called on it.

  2. 2
    During the constructor

    The initializer sets count_ to 0. This is the moment the object acquires its known starting condition.

  3. 3
    After creation

    The object is usable. count_ holds 0 and every member function can rely on that.

  4. 4
    Calling value

    value reads count_, finds 0, and returns it. Because the constructor already ran, the result is well defined.

Result

The object starts with count_ equal to 0, and value returns 0 with no extra checks needed inside the class.

Choosing Between Member and Standalone Functions

A function can be written as a member of a class, or it can be written outside the class as a standalone function. The source gives one test for choosing between them, and that test is about need rather than convenience. Make a function a member only if it needs direct access to the representation of the class. A function that must read or change the internal data of an object is a good candidate for membership. A function that can do its work using only the values it is given, without reaching into the object's internal representation, does not need to be a member, and the guidance is to keep it outside the class.

The difference shows up in what the function can see without being handed the data. A member function runs on an object and therefore has direct access to that object's stored state. A standalone function has no such built-in access, so any data it needs must be passed in as arguments. Keeping a function outside the class when it does not need the internal data keeps the class's interface focused on what truly depends on its internal representation.

Operators and Reader Expectations

An operator is a symbol such as plus or equals that a class can give a meaning to. Because one symbol can take on several implementations, the design guidance is to define operators primarily to mimic conventional usage. A reader who sees an operator applied to a class type should be able to guess its meaning from how that operator is normally understood. An operator that does something surprising, far from its usual meaning, makes the code harder to read and harder to trust.

In the real world

Suppose a class represents an amount of money and defines a plus operator. A reader who sees total = first plus second expects a new amount equal to the sum of the two. If the same plus symbol instead modified the first operand or printed something, the reader would have to stop and check the class definition before trusting any line that uses it. Matching the conventional meaning removes that check.

Promises Made by const Member Functions

The declaration of a function is a promise about behavior. A member function that only reads an object's state and never changes it should say so. When the function is declared const, the reader knows immediately that calling it leaves the object unchanged, which makes the code easier to reason about. The const word sits after the parameter list in a member function declaration, and it applies to the object the function is called on.

intreturn typevaluefunction name()parameter list, empty hereconstpromises the object is notchanged;end of declaration
Where does the const qualifier go in a member function declaration, and what does it say about the object that calls the function?
Before
int value();

Callers cannot tell from the declaration whether value changes the object.

After
int value() const;

The declaration states that value leaves the object unchanged, so a reader can rely on that.

cpp
Walk through it
  1. 1
    The const after value() tells the compiler and the reader that this function will not modify the object.
  2. 2
    increment has no const qualifier because its purpose is to change count_.
  3. 3
    In main, the object c is declared const, which means the program is not allowed to change it through any member function.
  4. 4
    Calling c.value() is therefore permitted, since value is the const function that promises not to change the object.
Notes on the code
  • value is declared const because it only reads count_
  • increment is not const because it changes count_
  • c is declared const, so only const member functions may be called on it

Predicting Which Calls Are Allowed on a const Object

Using the Counter class with a const object, decide which of the two calls will compile before you read the answer.

What do you think happens?

With const Counter c declared, will the call c.increment() compile?

  • Yes, because increment changes only count_

  • No, because increment is not declared const and c is const

Reveal answer
Answer:

No, because increment is not declared const and c is const

A const object may only be used through member functions declared const. The declaration of increment has no const qualifier, so the call is rejected. The call c.value() is accepted because value is declared const.

Mistakes Beginners Make with Classes and const

  • Common mistake

    Making every function a member by default

    What it looks like

    A function that only computes a result from the numbers it receives is written as a member even though it never reads the object's stored data.

    Why it fails

    The guidance is that membership is for functions that need direct access to the representation of the class. Extra members enlarge the class interface without any benefit.

    Do this instead

    Keep the function outside the class when it can do its work using only the values it is given.

  • Common mistake

    Leaving the constructor out and assuming objects start in a usable state

    What it looks like

    A class stores a count but has no constructor, so each member function must check whether the count was ever set.

    Why it fails

    Without a constructor, the object's starting condition is not guaranteed, and every function inherits the uncertainty.

    Do this instead

    Give the class a constructor that handles initialization, so that objects leave creation in a known condition.

  • Common mistake

    Declaring a read-only function without const

    What it looks like

    A function that only returns a stored value is declared without the const qualifier.

    Why it fails

    The declaration does not tell readers that the function leaves the object unchanged, and a const object cannot call it.

    Do this instead

    Add const after the parameter list of any member function that does not modify its object.

  • Common mistake

    Writing a symmetric operator as a member with surprising behavior

    What it looks like

    An operator is written only as a member, so an expression with the integer on the left does not compile.

    Why it fails

    A member function runs on the left-hand object, which privileges one operand. This breaks the expectation that both orders behave the same.

    Do this instead

    Write the symmetric operator as a nonmember function so both operands are treated on equal footing.

  • Common mistake

    Giving an operator an unconventional meaning

    What it looks like

    A plus operator on a class is defined so that it removes items rather than combining them.

    Why it fails

    Readers expect the conventional meaning, so a surprising definition makes code harder to read and trust.

    Do this instead

    Define operators primarily to mimic conventional usage.

Practice Deciding Membership and const

MEDIUM

For a class that stores a single integer, decide for each of the following whether it should be a member function, a nonmember function, or a const member function. (a) A function that returns the stored integer. (b) A function that adds two integers passed in as arguments and returns the sum. (c) A function that resets the stored integer to zero. Explain your choice for each using the membership test from this article.

Hints
  • Ask whether the function needs direct access to the stored integer.
  • A function that only reads the stored integer and never changes it can be declared const.
  • A function that changes the stored integer cannot be declared const.

Reviewing How the Pieces Fit Together

The ideas in this article form one design habit. The constructor makes every new object start in a usable state, so member functions can rely on that state. The membership test keeps a class focused on functions that truly need its internal data. Operators follow conventional meanings, and symmetric operators are written as nonmember functions so both operands are treated equally. Finally, a const member function declares that it leaves its object unchanged, which lets readers and the compiler trust that promise.

Key Takeaways

  • A constructor is a special member function used for object initialization, and it is where each new object is made to start in a usable state.
  • Make a function a member only when it needs direct access to the internal representation of the class; otherwise keep it outside the class.
  • Define operators to mimic conventional usage, and use nonmember functions for symmetric operators so both operands are treated on equal footing.
  • Declare a member function const when it does not modify its object, so the declaration tells readers that calling it leaves the object unchanged.

Related concepts

More concepts starting with C