11

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 Test2 object inside f (and that's the case in libc++ and in Visual Studio)

codicodi
  • 123
  • 1
  • 7
  • related/dupe: http://stackoverflow.com/questions/2431596/modifying-reference-member-from-const-member-function-in-c – NathanOliver Oct 27 '16 at 21:01
  • 1
    I think it is UB that `m_val` is modified. – Jarod42 Oct 27 '16 at 21:05
  • 6
    *"7.1.5.1/4: Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior."* – Jarod42 Oct 27 '16 at 21:11
  • 1
    And with `constexpr`, compilers found the error: [Demo](http://coliru.stacked-crooked.com/a/b1bcd6da0fdb81bb). :-) – Jarod42 Oct 27 '16 at 21:14

4 Answers4

8

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. In other words, these examples are well formed, but have undefined behavior.

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.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • Note that `m_val` (of const instance `t`) is modified. – Jarod42 Oct 27 '16 at 21:24
  • 1
    That's why we need the [`propagate_const`](http://en.cppreference.com/w/cpp/experimental/propagate_const) – vsoftco Oct 28 '16 at 01:37
  • So, is the example in the question legal or not? – MikeMB Jan 13 '20 at 07:17
  • @MikeMB: Both examples are well formed by have undefined behavior. – Jerry Coffin Jan 13 '20 at 07:53
  • @JerryCoffin: Thats my expectation too, but I'm missing that clear cut statement in your answer. Maybe I'm reading the question wrong, but as far as I can tell, the central question was never "Why does it compile?", but rather "is this well defined behavior according to the current standard?" (the last is a direct quote from the question) – MikeMB Jan 13 '20 at 15:34
  • 1
    @MikeMB: I guess from my perspective, the interesting question was mostly: "how should I write code that might have to deal with a situation like this", but I've added a clear statement for those who care about it. – Jerry Coffin Jan 13 '20 at 17:30
4

As others pointed out in comments: you are modifying object the m_ptr points to. This "pointed to" object is not a part of class Test (as far as compiler sees it). That's why compiler allows you to do so.

Having said that, I believe that it will be undefined behaviour. That's because m_ptr actually points to another member variable (m_val) of object const Test t! Compilers are allowed to optimize arggresively and they might rely on constness to do so.

The only exception is then you use mutable keyword, but it's another story.

Lehu
  • 761
  • 4
  • 14
2

Basically there are two types of constness in C++: physical constness and logical constness.

As for the physical constness everything is perfectly valid in the considered piece of code because set() modifies a value which is pointed by m_ptr and not the pointer itself which is a part of the class.

Logical constness is violated here. But there many ways in C++ to violate logical constness because this type of constness relies much on the particular class design.

In the example above program leads to UB because it tries to change a const object.

From the n4296, 7.1.6.1 The cv-qualifiers:

Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior.

Edgar Rokjān
  • 17,245
  • 4
  • 40
  • 67
1

It's undefined behavior. Not all const declared types are indeed constants, so it's not always undefined behavior to modify something declared that way. You could have a reference to a const type that refers to a non-const non-const value, cast away constness and modify the value without invoking undefined behavior. In this case though the original definition is const so you must assume it's a constant.

Modification of any constant is undefined behavior and yes, there's numerous ways to "accidentally" do so. In the aligned_storage version then yes, it's undefined behavior to modify that constant data by using placement new to modify it.

Edward Strange
  • 40,307
  • 7
  • 73
  • 125
  • placement-new creates a new object, it does not modify existing objects. (But there is a rule that using placement-new where there is/was a `const` object causes undefined behaviour) – M.M Oct 27 '16 at 21:36