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.
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.
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
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.
- 1The class declares a constructor named Counter, which has no return type, and its member initializer list sets count_ to 0 before the body runs.
- 2The public functions value and increment are the interface that the rest of the program uses.
- 3In main, the statement Counter c creates an object, and the constructor runs as part of that creation.
- 4Calling
c.value()is safe because the object was initialized first, so the function returns 0.
- 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 itincrement()changes the stored count and is therefore not const- main creates a Counter, so the constructor has already run before
value()is called
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?
- 1Before the constructor
The object is not yet usable. Its initialization is the constructor's job, and no member function has been called on it.
- 2During the constructor
The initializer sets count_ to 0. This is the moment the object acquires its known starting condition.
- 3After creation
The object is usable. count_ holds 0 and every member function can rely on that.
- 4Calling value
value reads count_, finds 0, and returns it. Because the constructor already ran, the result is well defined.
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.
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.
int value();Callers cannot tell from the declaration whether value changes the object.
int value() const;The declaration states that value leaves the object unchanged, so a reader can rely on that.
- 1The const after
value()tells the compiler and the reader that this function will not modify the object. - 2increment has no const qualifier because its purpose is to change count_.
- 3In main, the object c is declared const, which means the program is not allowed to change it through any member function.
- 4Calling
c.value()is therefore permitted, since value is the const function that promises not to change the object.
- 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?
Reveal 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 likeA 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 failsThe 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 insteadKeep 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 likeA class stores a count but has no constructor, so each member function must check whether the count was ever set.
Why it failsWithout a constructor, the object's starting condition is not guaranteed, and every function inherits the uncertainty.
Do this insteadGive 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 likeA function that only returns a stored value is declared without the const qualifier.
Why it failsThe declaration does not tell readers that the function leaves the object unchanged, and a const object cannot call it.
Do this insteadAdd 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 likeAn operator is written only as a member, so an expression with the integer on the left does not compile.
Why it failsA member function runs on the left-hand object, which privileges one operand. This breaks the expectation that both orders behave the same.
Do this insteadWrite 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 likeA plus operator on a class is defined so that it removes items rather than combining them.
Why it failsReaders expect the conventional meaning, so a surprising definition makes code harder to read and trust.
Do this insteadDefine operators primarily to mimic conventional usage.
Practice Deciding Membership and const
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.