More const voodoo

Hello everyone! I’m back with another post about const. Recently, I’ve learned of a strange behavior when it comes to const qualified member functions.


Consider the program:

#include <cstdio>
#include <memory>

class Foo
{
  private:
    int * x;
  public:
    Foo(int * x):x(x){}
    void bar(void) const
    {
      *x = 1;
      std::printf("%d\n", *x);
    }
};

int main(void)
{
  int x = 5;
  const Foo foo(std::addressof(x));
  foo.bar();
}

Do you think this program will compile?

Lets find out, g++ 8.3.0 tells me:

$ g++ k.cpp
$ ./a.out
1
$

I certainly did not think it would compile. Foo::bar() is const qualified and since x is modified I thought for sure this would fail to compile.

If I change this to:

#include <cstdio>
#include <memory>

class Foo
{
  private:
    int * x;
  public:
    Foo(int * x):x(x){}
    void bar(int * a) const
    {
      x = a;
      std::printf("%d\n", *x);
    }
};

int main(void)
{
  int x = 5;
  const Foo foo(std::addressof(x));
  int b = 10;
  foo.bar(std::addressof(b));
}

Rightly so, g++ 8.3.0 tells me:

$ g++ k.cpp
k.cpp: In member function ‘void Foo::bar(int*) const’:
k.cpp:12:11: error: assignment of member ‘Foo::x’ in read-only object
       x = a;
           ^

This to me indicates that within const qualified member functions member variables that are pointer types are considered T * const rather than const T * (or T const * if you’re an east-conster)

What??? Why is it this way???

This extends out to references also:

#include <cstdio>

class Foo
{
  private:
    int & x;
  public:
    Foo(int & x):x(x){}
    void bar(void) const
    {
      x = 1;
      std::printf("%d\n", x);
    }
};

int main(void)
{
  int x = 5;
  const Foo foo(x);
  foo.bar();
}

The above example will compile and give the same result as the first.


Let’s try to explain this behavior. I know that const qualifying a member function makes it so that the implicit this pointer is const qualified. Lets change our example to make this more obvious.

#include <cstdio>
#include <memory>

struct Foo
{
  int * x;
};

void bar(const Foo * this_)
{
  *this_->x = 1;
  std::printf("%d\n", *this_->x);
}

int main(void)
{
  int x = 5;
  const Foo foo{std::addressof(x)};
  bar(std::addressof(foo));
}

This makes it clearer why the original code is working, but it’s still not clear to me why when we reach through a const Foo * the int * x isn’t also const.


If the code is changed to something like:

#include <cstdio>
#include <memory>

struct Foo
{
  int * x;
};

void bar(const Foo * this_)
{
  static int a = 5;
  this_->x = &a;
  std::printf("%d\n", *this_->x);
}

int main(void)
{
  int x = 5;
  const Foo foo{std::addressof(x)};
  bar(std::addressof(foo));
}

Then g++ 8.3.0 tells me:

$ g++ k.cpp
k.cpp: In function ‘void bar(const Foo*)’:
k.cpp:12:15: error: assignment of member ‘Foo::x’ in read-only object
   this_->x = &a;
               ^

So it does seem like it’s treated as int * const.


I change the code once more to use a reference:

#include <cstdio>
#include <memory>

struct Foo
{
  int & x;
};

void bar(const Foo * this_)
{
  this_->x = 1;
  std::printf("%d\n", this_->x);
}

int main(void)
{
  int x = 5;
  const Foo foo{x};
  bar(std::addressof(foo));
}

then this works fine, but this time I can’t explain where the const has gone. I assume this works because the machine code under the hood works the same way as before. Disecting the mess that is Built-in member access operators leaves me with more questions than answers.


If we rewrite the example using strict C

#include <stdio.h>

struct Foo
{
  int * x;
};

void bar(const struct Foo * this_)
{
  *this_->x = 1;
  printf("%d\n", *this_->x);
}

int main(void)
{
  int x = 5;
  struct Foo foo;
  foo.x = &x;
  bar(&foo);
}

then I can get some clarity from C member access through pointer which clearly says

If the type pointed to by the left operand is const or volatile qualified, the result is also qualified.

One day I’ll get the motivation to clarify that the C++ definition has equivalent language to mean the same thing.

So all in all with these examples we’ve come to learn of a, at least in my opinion, strange quark in C++ and C.


In regards to C++ there is one addition that can help remove this weirdness. std::experimental::propagate_const makes it so that pointers and references give access to const objects when acted on in const methods.

If we look back on the original code we started with:

#include <cstdio>
#include <memory>

class Foo
{
  private:
    int * x;
  public:
    Foo(int * x):x(x){}
    void bar(void) const
    {
      *x = 1;
      std::printf("%d\n", *x);
    }
};

int main(void)
{
  int x = 5;
  const Foo foo(std::addressof(x));
  foo.bar();
}

We can change it to the following:

#include <cstdio>
#include <memory>
#include <experimental/propagate_const>

class Foo
{
  private:
    std::experimental::propagate_const<int *> x;
  public:
    Foo(int * x):x(x){}
    void bar(void) const
    {
      *x = 1;
      std::printf("%d\n", *x);
    }
};

int main(void)
{
  int x = 5;
  const Foo foo(std::addressof(x));
  foo.bar();
}

Now g++ 8.3.0 tells me:

$ g++ k.cpp
k.cpp: In member function ‘void Foo::bar() const’:
k.cpp:13:12: error: assignment of read-only location ‘((const Foo*)this)->Foo::x.std::experimental::fundamentals_v2::propagate_const<int*>::operator*()’
       *x = 1;
            ^

Which makes all well in the world for me. :) <3

Written on August 8, 2021