Shallow Constness

Once awhile, I see programmers who are new to C++ frustrated by the use of the const qualifiers on member functions. These frustrations usually reduce to the following example.

struct X { int i; };
class Y
{
public:
	Y() { m2 = &m1; } // m2 points to m1
	X *M1() const { return &m1; } // This won't compile.
	X *M2() const { return m2; }  // This does.
private:
	X m1;
	X *m2;
};

When it comes to this, there are two camps of programmers.

  1. C++ is so inconsistent! M2() is fine, but why won’t M1() compile? I am clearly not modifying m1.
  2. C++ is so inconsistent! M1() is fine, but why would M2() compile? This is clearly a constness loophole because people can modify the content of m2.

Believe it or not, C++ is actually very consistent. It is just not very intuitive.

The “this” Pointer

The behavior can be traced back to the this pointer, and the side effects of the const qualifier on the member function.

In the C++ standard section 9.3.2.1

… If a member function is declared const, the type of this is T const*, if the member function is declared volatile, the type of this is T volatile *, and if the member function is declared const volatile, the type of this is  T const volatile *.

So in the example, the this pointer has type Y const *, which reads pointer to a const Y object.

Expand for Details

Now that we know the type of the this pointer, we can expand M1() and M2().

Let’s start with M1(). Since the this pointer is of type Y const *, this->m1 will inherit the const qualifier, and is of type X const.

X *M1() const
{
   // this has type Y const * ;
   X const tmp = this->m1; // this->m1 has type X const;
   X const *tmpAddr = &tmp;// &this->m1 has type X const *;
   X *tmp2 = tmpAddr;      // Can't compile! Can't copy X const * to X *.
   return &tmp2;
}

In line 6, the compiler fails to copy X const * to X *. In other words, the compiler can’t convert a “pointer to a const X” to a “pointer to X”. This is consistent with the definition of the const qualifier. Hence, M1 fails to compile.

For M2(), we can expand the function in a similar way.

X *M2() const
{
   // this has type Y const * ;
   X *tmp = this->m2; // this->m2 has type X * const;
   return tmp;
}

Unlike M1, it is perfectly legal to convert X * const to X*.  In other words, the compiler can copy a “const pointer to X” to a “pointer to X”. This is also consistent with the definition of the const qualifier.

But That’s Not The Point

Unfortunately, the answer above rarely satisfies the frustrated programmers. They are trying to follow the guidelines of const-correctness, and this behavior, although consistent, is ambiguous and makes no sense.

So here’s my recommendation – program defensively.

If you are going to return a member variable pointer (including smart ptr) or reference in a member function, never apply the const qualifier to the member function. Since C++ constness is shallow, the const qualifier only provides a false sense of security.  By assuming the worst, it will always be consistent and intuitive.