5.2.3 · D4C++ Programming

Exercises — const correctness — const variables, const pointers, const member functions

2,343 words11 min readBack to topic

The one rule that solves almost everything on this page:

Figure — const correctness — const variables, const pointers, const member functions

Level 1 — Recognition

Exercise 1.1

For each declaration, say what can change: the pointed-to value (*p), the pointer (p), both, or neither.

const int*       p1;
int* const       p2 = &a;
const int* const p3 = &a;
int*             p4;
Recall Solution

Read each right-to-left.

  • p1 — "p1 is a pointer to a const int." Pointee locked, pointer free. Can change p1, not *p1.
  • p2 — "p2 is a const pointer to an int." Pointer locked, pointee free. Can change *p2, not p2.
  • p3 — "p3 is a const pointer to a const int." Neither.
  • p4 — plain pointer to int. Both.

Count of "can change *p" cases: p2, p42. Count of "can change p" cases: p1, p42.

Exercise 1.2

Which of these lines compile? (Assume int a = 1, b = 2; above.)

const int x = 5;   // A
const int y;       // B
int z;             // C
z = 4;             // D
x = 9;             // E
Recall Solution
  • A compiles — const, initialized. ✅
  • B fails — a const must be initialized at declaration. ❌
  • C compiles — plain int, no init required. ✅
  • D compiles — z is not const. ✅
  • E fails — can't assign to a const. ❌

Compiling lines: A, C, D → 3 of 5.


Level 2 — Application

Exercise 2.1

Given int a = 10, b = 20;, mark each line OK or ERROR:

const int* p = &a;
*p = 99;      // (1)
p  = &b;      // (2)
 
int* const q = &a;
*q = 99;      // (3)
q  = &b;      // (4)
Recall Solution

p is pointer to const int: pointee locked, pointer free.

  • (1) *p = 99;ERROR (writing through pointer-to-const).
  • (2) p = &b;OK (repointing is allowed).

q is const pointer to int: pointer locked, pointee free.

  • (3) *q = 99;OK.
  • (4) q = &b;ERROR (repointing a const pointer).

Errors: (1) and (4). OK: (2) and (3).

Exercise 2.2

Write the parameter type for a function print that reads a C-string but promises never to modify it, and can accept a caller's const char[]. Then say why the non-const version void print(char* s) fails a const caller.

Recall Solution
void print(const char* s);   // pointer to const char

The pointee is const, so inside print you may read s[i] but not write it. Because the pointee type is const char, a caller may pass a const char[] — the const-ness matches.

void print(char* s) demands a pointer to mutable char. Passing const char[] would drop the caller's const promise, which the compiler forbids. So the const caller can't use it. Answer: const char*.


Level 3 — Analysis

Exercise 3.1

Consider:

class Circle {
    double r;
public:
    double area() const { return 3.14159 * r * r; }
    void   setR(double x) { r = x; }
};
 
const Circle c{};   // r defaults to 0.0
c.area();   // (1)
c.setR(5);  // (2)

Which calls compile? What type does this have inside area()? Compute the value returned by c.area().

Recall Solution
  • c is a const object, so its this is const Circle*.
  • (1) area() is a const member function → its this is const Circle*, which matches. OK.
  • (2) setR is non-const → its this is Circle*, cannot bind to a const object. ERROR.
  • Inside area(), this has type const Circle* (data members read-only).
  • r was value-initialized to 0.0, so area() returns .

Compiles: (1) only. Returned value: 0.0.

Exercise 3.2

Explain why this class needs two at overloads, and predict which one each call below selects.

class Buffer {
    char data[100];
public:
    char&       at(int i)       { return data[i]; }
    const char& at(int i) const { return data[i]; }
};
 
Buffer       mb;
const Buffer cb{};
mb.at(0) = 'X';   // (1)
char x = cb.at(0); // (2)
cb.at(0) = 'Y';   // (3)
Recall Solution

Overload resolution uses the object's const-ness to pick the this type.

  • (1) mb is non-const → selects char& at(int) → returns a writable ref → assignment OK.
  • (2) cb is const → selects const char& at(int) const → returns read-only ref → reading is OK.
  • (3) cb is const → selects the const overload returning const char&; you can't assign to a const char&. ERROR.

Two overloads exist so a const object gets a read-only view while a non-const object gets a mutable one — exactly how std::vector::operator[] works (see const operator[]). OK: (1),(2). ERROR: (3).


Level 4 — Synthesis

Exercise 4.1

Make this class const-correct. magnitude() should be callable on a const Vec2, and it should cache its result so a second call doesn't recompute — without breaking const. List every change.

class Vec2 {
    double x, y;
    double cached;   // last computed magnitude
    bool   valid;    // is cache fresh?
public:
    Vec2(double x_, double y_) : x(x_), y(y_), valid(false) {}
    double magnitude() {
        if (!valid) { cached = sqrt(x*x + y*y); valid = true; }
        return cached;
    }
};
Recall Solution

magnitude() only logically reads the vector — the cache is an implementation detail, not part of the observable state. So mark it const, and make the two cache fields mutable so they may still be written inside a const method.

class Vec2 {
    double x, y;
    mutable double cached;   // (change 1) mutable
    mutable bool   valid;    // (change 2) mutable
public:
    Vec2(double x_, double y_) : x(x_), y(y_), valid(false) {}
    double magnitude() const {              // (change 3) const method
        if (!valid) { cached = sqrt(x*x + y*y); valid = true; }
        return cached;
    }
};

Three changes: mark cached and valid mutable, add const after magnitude()'s parameter list.

Sanity check the number: Vec2(3,4).magnitude() returns , and calling it twice returns 5.0 both times (second call uses the cache).

Exercise 4.2

For Vec2 v(3,4); and const Vec2 cv(5,12);, state which calls compile and their numeric results (using the const-correct class above).

v.magnitude();    // (1)
cv.magnitude();   // (2)
Recall Solution
  • (1) v non-const → magnitude() const still callable (a const method works on non-const objects too). Returns . OK.
  • (2) cv const → magnitude() is const, matches. Returns . OK.

Both compile. Results: 5.0 and 13.0. Note: a const method is the superset — callable on both const and non-const objects. That is why you mark read-only methods const up front.


Level 5 — Mastery

Exercise 5.1

Classify each snippet as well-defined, compile error, or undefined behavior (UB).

// A
const int k = 42;
int* p = const_cast<int*>(&k);
*p = 7;                          // ?
 
// B
int m = 42;
const int* cp = &m;
int* p = const_cast<int*>(cp);
*p = 7;                          // ?
 
// C
const int k = 42;
int&       r = const_cast<int&>(k);   // ?
Recall Solution
  • Ak was originally declared const. Casting away const and writing through the result is undefined behavior. It may "work", crash, or corrupt neighbouring data — never rely on it.
  • B — the underlying object m is actually non-const; cp merely pointed to it via a const view. Casting the const away and writing is well-defined: m becomes 7.
  • C — this line only forms the reference; the cast itself compiles. But it compiles (not an error). Writing r = ... afterward would be UB because k is truly const — as written (no write yet), the line is a legal but dangerous binding.

A = UB, B = well-defined (m becomes 7), C = compiles (writing through r would be UB).

Exercise 5.2

A struct holds a raw pointer. Show what a const object freezes and what it does not.

struct Holder {
    int*  ptr;
    int   value;
};
int backing = 10;
const Holder h{ &backing, 99 };
 
h.value  = 1;      // (1)
h.ptr    = nullptr;// (2)
*h.ptr   = 55;     // (3)
Recall Solution

On a const Holder, every member becomes const bitwise:

  • valueconst int(1) ERROR (can't write a const member).
  • ptrint* const (const pointer, mutable pointee) → (2) ERROR (can't repoint a const pointer).
  • *h.ptr writes through to backing, which is a separate, non-const object → (3) OKbacking becomes 55.

This is shallow constness in one picture: the pointer is frozen, the pointed-to data is not. (1) ERROR, (2) ERROR, (3) OK → backing == 55.


Score yourself

Recall Answer key (one line each)

Ex 1.1 :::: p1=change pointer, p2=change value, p3=neither, p4=both Ex 1.2 :::: A,C,D compile (3 of 5) Ex 2.1 :::: ERROR,OK,OK,ERROR Ex 2.2 :::: const char* Ex 3.1 :::: only area() compiles; this is const Circle*; returns 0.0 Ex 3.2 :::: (1) OK writable, (2) OK read, (3) ERROR Ex 4.1 :::: mutable cached, mutable valid, const on magnitude() Ex 4.2 :::: 5.0 and 13.0, both compile Ex 5.1 :::: A=UB, B=well-defined (m=7), C=compiles (later write is UB) Ex 5.2 :::: (1) ERROR, (2) ERROR, (3) OK, backing=55

Recall Feynman recap

const on a variable freezes the name. const left of * freezes the pointee; right of * freezes the pointer. const after a method freezes this so the method may only read. mutable unfreezes a chosen member for bookkeeping. Casting away a genuinely const object and writing = undefined behaviour. Nothing else to memorize — just read right-to-left.