codicodi codicodi - 1 month ago 6
C++ Question

Modifying const object through pointer obtained during construction

I just discovered how easy it is to modify const objects without any

const_cast
black magic. Consider:

#include <iostream>

class Test {
public:
Test(int v)
:m_val{ v },
m_ptr{ &m_val }
{}

int get() const { return m_val; }
void set(int v) const { *m_ptr = v; }

private:
int m_val;
int* m_ptr;
};

int main()
{
const Test t{ 10 };

std::cout << t.get() << '\n';
t.set(0);
std::cout << t.get() << '\n';

return 0;
}


Recent versions of Clang, GCC, and MSVC don't show any warning and produce expected output:


10 0


Is this well defined behavior according to the current standard? If it's undefined what if
m_val
was of type
std::aligned_storage_t<sizeof(int), alignof(int)>
and constructor
new
'ed
int
in it? I believe it's pretty common case when it comes to small buffer optimizations.

Edit

Thanks, it seems that it's just another way to shoot yourself in a foot.
What's troubling it seems that this:

struct Test2 {
int i;
void operator()() { ++i; }
};

const std::function<void()> f{ Test2{ 10 } };
f();


is also undefined behavior when implementation chooses to store the
Test
object inside
f
(and that's the case in libc++ and in Visual Studio)

Answer

const enforces "bitwise constness", but what you usually want is "logical constness".

In the case of an object that contains a pointer, this means that a const member function can't modify the pointer itself, but can modify what the pointer refers to.

This has been well known for a long time.

To get logical constness, you 1) use mutable (or sometimes const_cast) to allow modification of members that don't affect the object's logical state (e.g., cached values/memoization), and 2) generally have to manually enforce not writing to data through a pointer (but if it's an owning pointer, that ownership should probably be delegated to an object that only manages ownership of that data, in which case making it const should normally prevent writing to the data it owns).

As far as the specific detail of having a non-const pointer pointing to data that might itself have been const modified, well, you're basically just getting a (persistent) version of roughly the same thing that const_cast is typically used to do: get non-const access to data to which you'd otherwise only have a const pointer. It's up to you to ensure that you only use this in ways that doesn't cause a problem (but just having and/or writing through that pointer doesn't, in itself, necessarily lead to a problem).

In other words, what we have here are two separate pointers to some data. this lets you access an object's data. In a const member function, you can only read (not) write data via this, unless (as noted above) it's marked mutable. In this case, you're saving a second pointer to the same data. Since there's nothing to mark that as a pointer to const, it's not, so you get non-const access to the data it points at.