Does this code subvert the C++ type system?
Asked Answered
F

5

20

I understand that having a const method in C++ means that an object is read-only through that method, but that it may still change otherwise.

However, this code apparently changes an object through a const reference (i.e. through a const method).

Is this code legal in C++?

If so: Is it breaking the const-ness of the type system? Why/why not?

If not: Why not?

Note 1: I have edited the example a bit, so answers might be referring to older examples.

Edit 2: Apparently you don't even need C++11, so I removed that dependency.

#include <iostream>

using namespace std;

struct DoBadThings { int *p; void oops() const { ++*p; } };

struct BreakConst
{
    int n;
    DoBadThings bad;
    BreakConst() { n = 0; bad.p = &n; } 
    void oops() const { bad.oops(); }  // can't change itself... or can it?
};

int main()
{
    const BreakConst bc;
    cout << bc.n << endl;   // 0
    bc.oops();              // O:)
    cout << bc.n << endl;   // 1

    return 0;
}

Update:

I have migrated the lambda to the constructor's initialization list, since doing so allows me to subsequently say const BreakConst bc;, which -- because bc itself is now const (instead of merely the pointer) -- would seem to imply (by Stroustrup) that modifying bc in any way after construction should result in undefined behavior, even though the constructor and the caller would have no way of knowing this without seeing each others' definitions.

Firn answered 18/6, 2012 at 5:57 Comment(9)
This is somewhat off-topic, but void (void) is a deprecated construct since void () has done the same thing since C++98 and C99.Poikilothermic
@moshbear: I have no idea what made me write that (backwards compatibility with C or something?!); fixed, thanks. :-)Firn
I like how your O:) smiley has a double meaning as a happy angel and a shocked horrified face, depending on which direction you read it from.Khartoum
You may find more inputs here: stackoverflow.com/q/9939399/15161 (Similar kind of question that people doesn't like :) )Gunfight
Here is my question inspired by this #11091885 also it doesnt require another struct ;)Henceforward
@acidzombie24: lol nice job removing the need for the struct, but IMO it's pretty much just a dupe of my question. :P It's going to generate exactly the same kind of discussion as here...Firn
@Mehrdad when i first read your question (theres a lot of edits since i last read it) i thought you were asking from a design point of view (quoting you "If so: Is it breaking the const-ness of the type system? Why/why not?"). If you feel its too close then close mine as a dupeHenceforward
@acidzombie24: Hmm... tough to say. What I'm asking about, more specifically, is whether my example is indeed a "hole" in the type system (implying I didn't violate anything, but the end result is still undefined), or whether there is some rule which I have accidentally violated at some point (hence the "is it breaking the system [or did I violate a rule]?" part). Tough to say whether yours is a dupe I guess (that's why I was suggesting a clarification to show the difference), so I'll just leave it to others to decide if you don't know of anything else to add. :)Firn
@acidzombie24: And yes, it's also from a design perspective in the sense of: how is the caller supposed to know that an object shouldn't be const?Firn
C
12

The oops() method isn't allowed to change the constness of the object. Furthermore it doesn't do it. Its your anonymous function that does it. This anonymous function isn't in the context of the object, but in the context of the main() method which is allowed to modify the object.

Your anonymous function doesn't change the this pointer of oops() (which is defined as const and therefore can't be changed) and also in no way derives some non-const variable from this this-pointer. Itself doesn't have any this-pointer. It just ignores the this-pointer and changes the bc variable of the main context (which is kind of passed as parameter to your closure). This variable is not const and therefore can be changed. You could also pass any anonymous function changing a completely unrelated object. This function doesn't know, that its changing the object that stores it.

If you would declare it as

const BreakConst bc = ...

then the main function also would handle it as const object and couldn't change it.

Edit: In other words: The const attribute is bound to the concrete l-value (reference) accessing the object. It's not bound to the object itself.

Chirpy answered 18/6, 2012 at 6:20 Comment(30)
Would the same reasoning apply if, instead of making the anonymous method inside main, I said BreakConst() : fn([&]() { counter++; }), counter(0) { } in the constructor? In that case it looks like the method would be "in the context of the object", but I dunno...Firn
@Mehrdad: Good comment. I was going to ask this.Antitoxic
Heinzi: I would be interested to see some text from the Standard which supports your reasoning.Antitoxic
@Mehrdad: In this case you have the scope of the constructor in which you are also allowed to change the counter. If you take the non-const reference to counter from the constructor and store it in a non-const variable (like the closure scope), you are allowed to change it. The same applies here: struct Test { Test():_const(0),_nonconst(&_const) {} int *_nonconst; int _const; }; int main() { Test test; cout << test._const; ++*test._nonconst; cout << test._const; }Chirpy
@Heinzi: I guess the problem I have with that reasoning is that if I migrate the lambda to the constructor, I could subsequently say const BreakConst bc;, which -- because bc itself is now const (instead of just the pointer) -- would seem to imply that modifying bc in any way after construction should result in UB, even though the constructor and the caller would have no way of knowing this without seeing each others' definitions. Is this correct?Firn
C++ never specifies objects itself as const. If you say const BreakConst bc; then only your l-value "bc" is const. If you use this in a normal way, the type system assures that you don't get a non-const reference to the object and therefore you can't change it. But the object itself isn't const (there is no flag stored on it or something like that). You can always get a non-const reference using const_cast.Chirpy
@Chirpy I suggest making the IOW part bold. I'd queue an edit, but adding two asterisks around each side doesn't meet the six character requirement.Poikilothermic
@Heinzi: But your statement goes against this one, by Stroustrup: An object declared const is considered immutable from the completion of the constructor until the start of its destructor. The result of a write to the object between those points is deemed undefined.Firn
Note to my example above: The main function should start with int main() { const Test test; ...} to be the appropriate example.Chirpy
@Heinzi: Yup, I just updated my question to match this issue.Firn
I just debugged the BreakConst() : fn([&]() { counter++; }), counter(0) { } code with VC 2010, and realized that counter inside the lambda does not refer to this->counter but to this->__this->counter ... can anyone explain __this to me? It does not change the object in the main function. Now I am really confused ...Amitie
@Mehrdad: I'm not sure about this stroustrup cite. Is it part of the standard? If yes, I can imagine why. It doesn't change anything on the implementation of constness (It would be nonsense to store a const flag at the object), but if an object is non-writeable, the compiler could be allowed to do some further optimizations that aren't possible, when the object is mutable. If a compiler implements those, writing to the object would result in undefined behaviour. Then you would get some problems with your code...Chirpy
@Vash: I assume the __this is the pointer to the closure context. This is the way the bc variable of the main function is passed to the anonymous function.Chirpy
@Heinzi: Well I mean Stroustrup is the guy who designed C++, so... I take his word almost as face value as I do for the standard. :) So if you think he's made a mistake (or perhaps the person who posted that quote...), that's certainly a possibility, but not one I'd be convinced of without a bit more support, unfortunately.Firn
The cite says that it results in undefined behaviour. This presumes that you are allowed to change it by the compiler (for example using your way or const_cast), but shouldn't - maybe because of compiler optimizations regarding this object as immutable.Chirpy
@Chirpy ok this is correct, I thought it would access the object from the main function, but this is not correct anymore. In this case another object is constructed. I think this is dangerous code :DAmitie
@Heinzi: Huh? I don't understand your statement. When the C++ standard says "undefined behavior" that does not mean "you can but you shouldn't"... it's saying "you must not, but we really have no way to enforce the rule, so if you violate it, we guarantee nothing whatsoever about your program's execution (or lack thereof)". So over here, it doesn't matter whether it's "by the compiler" or through the use of some black magic -- if they say modifying an immutable object is UB, then it's UB, no matter how that happens. (Or at least that's how I understand it.)Firn
@Vash: Copying the bc of the main function to a new object is necessary, because the anonymous function could be called after the main function returned (and its bc instance is destroyed). If bc - like in this case - is a real object, then your anonymous function still will result in undefined behaviour, because it's then accessing an invalid object. But if bc would just be a pointer or a primitive type, then copying it prevents this.Chirpy
@Mehrdad: This is what I meant by "You can but you shouldn't". "You can" = "The compiler allows you to"Chirpy
@Heinzi: So just so I understand correctly: Is your final answer that this is undefined behavior? (If so, can you point out a precise place at which I invoke UB, instead of just pointing to the entire source code?)Firn
I'm not sure if this cite is part of the standard. If it is, you would invoke undefined behaviour. It's hard to point to one code piece. Just creating an object that modifies itself isn't undefined behaviour. Just making an object const also isn't. But according to the cite, the combination is. I think on most (if not all) current plattforms it will work as expected. But if you get a plattform like the one mentioned in stroustrups cite (where const objects are stored in hardware const memory), you will get a problem.Chirpy
@Heinzi: Yeah, that's the problem I'm having. The issue is that the writer of the constructor (and the caller) seem to have no way of knowing whether the other one might violate const-ness, as they could be in different files. So it seems a little weird to me that this might be undefined, but... yeah...Firn
Don't keep the non-const reference to the this parameter of the constructor outside of the constructor. The constructor/destructor are the only functions that are allowed to change const objects. So all other functions you use to change them will behave according to the const attribute.Chirpy
@Heinzi: What if you need to store a reference somewhere else? (The constructor has no idea that the object is const.) And what if the guy using the pointer is a piece of code written later (perhaps a subclass), who has no idea that the pointer is referring to something const? etc... so many possibilities...Firn
You never have to. There is always another way solving the problem. For example using an init() method additional to the constructor. If you pass the non-const pointer to a subclass, you pass it outside of the constructor what breakes my rule of not using them outside of the constructor.Chirpy
@Chirpy Since no one else seems to have contradicted this: C++ very definitely does have const objects, and trying to modify one is undefined behavior (except for mutable members of the object).Weitzel
@JamesKanze: Yup. The question for me now is, is this (indeed) breaking const? It seems to me that in order for this to have been intended to be UB, we'd need to be able to point to a specific line and say: you can't do that! But it seems from the discussions that my example is playing perfectly by the rules at every line -- it's just that the overall effect turns out to be something undefined. Which kind of scares me, since it seems to be impossible to detect from just looking at declarations. Does that mean I just 'broke' const?Firn
@Mehrdad You can't always point to just a single line. If you could, the compiler could reliably detect it, and the committee probably would have made it a required diagnostic, rather than undefined behavior. Your code attempts to modify a const object, which means that it has undefined behavior.Weitzel
@JamesKanze: Hmm... that's a little weird to me. Say you have this class. How is the user of the class supposed to know that he isn't supposed to declare a const instance of it (either directly or through another object that contains it)?Firn
@Mehrdad Documentation? Or simply don't write such classes. Just because you can subvert the type system, doesn't mean that you have to.Weitzel
S
3

You code is correct, because you don't use the const reference to modify the object. The lambda function uses completely different reference, which just happen to be pointing to the same object.

In the general, such cases does not subvert the type system, because the type system in C++ does not formally guarantee, that you can't modify the const object or the const reference. However modification of the const object is the undefined behaviour.

From [7.1.6.1] The cv-qualifiers:

A pointer or reference to a cv-qualified type need not actually point or refer to a cv-qualified object, but it is treated as if it does; a const-qualified access path cannot be used to modify an object even if the object referenced is a non-const object and can be modified through some other access path.

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.

Substitutive answered 18/6, 2012 at 6:20 Comment(9)
A const object? Where'd that come from? I thought just the pointer/reference was const.Firn
@Mehrdad: const Foo f; declares a const objectAntimicrobial
As my quote from the standard says - the const reference is treated as it would point to a const object.Satirize
@RafałRawicki: Hmm, that's interesting... so you're saying this is UB? I guess then my question is, why exactly is it UB? (i.e. at what part of the code, precisely, did I violate anything?)Firn
I answered referring to the question about subverting the typesystem itself. On the second thought your code sample does not use the const reference, so it is correct. This is a good example of why formally proving that the function (or we should rather say a function call) is pure is very hard.Satirize
-1. The "access path" is via bc in main, bound to a lambda, and therefore non-const.Irs
@Irs I've edited my answer and wrote that below the standard quote before you gave me -1.Satirize
@RafałRawicki: Now it's unclear how you arrive at the conclusion that it is Undefined Behavior.Irs
@Irs I think, I made my answer more clear now, I was focused more on the type system correctness, than this concrete example. Thanks for guidance.Satirize
C
3

I already saw something similar. Basically you invoke a cost function that invoke something else that modifies the object without knowing it.

Consider this as well:

#include <iostream>
using namespace std;

class B;

class A
{
    friend class B;
    B* pb;
    int val;
public:
    A(B& b); 
    void callinc() const;
    friend ostream& operator<<(ostream& s, const A& a)
    { return s << "A value is " << a.val; }
};

class B
{
    friend class A;
    A* pa;
public:
    void incval() const { ++pa->val; }
};

inline A::A(B& b) :pb(&b), val() { pb->pa = this; }
inline void A::callinc() const { pb->incval(); }


int main()
{
    B b;
    const A a(b);  // EDIT: WAS `A a(b)`
    cout << a << endl;
    a.callinc();
    cout << a << endl;
}

This is not C++11, but does the same: The point is that const is not transitive.

callinc() doesn't change itself a and incval doesn't change b. Note that in main you can even declare const A a(b); instead of A a(b); and everything compile the same.

This works from decades, and in your sample you're just doing the same: simply you replaced class B with a lambda.

EDIT

Changed the main() to reflect the comment.

Celebrity answered 18/6, 2012 at 6:31 Comment(2)
Your example doesn't address the issue of a const object though (after my updates to the question, based on another answer). That's what's scaring me.Firn
@Mehrdad: See my edit. Is that what you meant? It works! (Or ... It doesn't, since according to your conception, it shouldn't)Celebrity
W
2

The issue is one of logical const versus bitwise const. The compiler doesn't know anything about the logical meaning of your program, and only enforces bitwise const. It's up to you to implement logical const. This means that in cases like you show, if the pointed to memory is logically part of the object, you should refrain from modifying it in a const function, even if the compiler will let you (since it isn't part of the bitwise image of the object). This may also mean that if part of the bitwise image of the object isn't part of the logical value of the object (e.g. an embedded reference count, or cached values), you make it mutable, or even cast away const, in cases where you modify it without modifying the logical value of the object.

Weitzel answered 18/6, 2012 at 8:10 Comment(0)
R
0

The const feature merely helps against accidental misuse. It is not designed to prevent dedicated software hacking. It is the same as private and protected membership, someone could always take the address of the object and increment along the memory to access class internals, there is no way to stop it.

So, yes you can get around const. If nothing else you can simply change the object at the memory level but this does not mean const is broken.

Recipient answered 18/6, 2012 at 8:56 Comment(14)
The const feature merely helps against accidental misuse -- I guess the question is, why doesn't this qualify as "accidental misuse"?Firn
It comes more under my next sentence "It is not designed to prevent dedicated software hacking" ;-) There is no way to prevent data attacks at the memory level.Recipient
What do you mean by "at the memory level"? What other levels are there?Firn
For example, if you declare a class and name a private member I cannot modify it with code from another class (unless you take steps to allow it e.g. make me a friend). It is safe at the code level. I can, however, take the address of your object and access your member at the memory level and change it. There is no way of stopping this. This does not mean that 'private' is broken, merely that it was intended to prevent casual misuse, not a dedicate attack.Recipient
Hmm... that's a great point, but it's a slightly different point. accessing something private does not cause undefined behavior, as far as I know -- but const does! So it seems reasonable to me that if something causes UB, you're supposed to be able to protect against it somehow, but I don't see how you can do that here.Firn
I thought the principles were the same? It is up to the programmer to not try to get around these constructs, you can easily access an unitialised pointer and get UB. The compiler and standards only offer basic level protection (const, access levels etc). If users sidestep these then they are on their own.Recipient
The principles are the same, but the implications aren't. In the case of private, it just means that you got around the protection.... okay, whatever. In the case of const, it means that there is no way to tell if making a const object of a particular class will give you defined behavior, which jeopardizes your own code! That's a pretty radical difference to me.Firn
"The principles are the same, but the implications aren't. In the case of private" Ah, in that case we are in violent agreement! I was only refering to the principles (of merely protecting against causual misuse and not a code attack). I was not addressing the differing potential severity of the situations, I agree that in terms of severity or a causing a potential genuine accident they are different.Recipient
I guess I always thought that if you play by the rules (which is admittedly hard), C++ can at least give you certain guarantees. Now you're telling me that's not true -- your code can blow up even if you play entirely by the rules? That's a new concept to me -- to me, that would imply that this breaks the type system, since even when I am playing by the rules, it's still blowing up my program.Firn
Are you sure it is a new concept? If you declare a pointer and access it without initialising it then you have not broken a specific compiler/language rule but you would be lucky to not blow up.Recipient
"Lucky not to blow up" != "unlucky to blow up" though. I was never talking about expecting "undefined behavior" to give (or not to give) reasonable results. Obviously, it's undefined. :-) I was asking about reasonable behavior giving undefined results.Firn
Then we appear to be talking about objective v subjective (ie 'reasonable behaviour' is subjective). The objective fact is that these language constructs (like the type system) do not give absolute protection, particularly is someone is trying to get around them. Subjectively, some might be easier to get around than others by accident or by malicious coding techniques but that is irrelevant to my point.Recipient
Perhaps -- but I'm not really convinced yet that const is supposed to be so subjective. We've heard of code being "const-correct", which is (supposedly) objectively checkable. I've never heard of code being "private-correct" (or whatever). That by itself tells me there must be a way of checking the correctness of code that uses const, just from the declarations (and not the implementations of the code called). The fact that people never say "private-correct", etc. tells me that the analogy with other protections and undefined behavior might not be 100% accurate... but perhaps I'm wrong.Firn
Sorry, I was not refering to const being subjective. I was refering to your point about reasonbale beheaviour, which is subjective. Const is a single concept hence you have 'const correct'. Private access would be a component of the concept of encapsulation and data hiding, which is probably why the phrase 'private correct' is not used.Recipient

© 2022 - 2024 — McMap. All rights reserved.