Is it legal to modify an object created with new through a const pointer?
Asked Answered
R

3

15

So this answer made me think about the scenario where you assign the result of new to a pointer to a const. AFAIK, there's no reason you can't legally const_cast the constness away and actually modify the object in this situation:

struct X{int x;};

//....
const X* x = new X;
const_cast<X*>(x)->x = 0; // okay

But then I thought - what if you actually want new to create a const object. So I tried

struct X{};

//....
const X* x = new const X;

and it compiled!!!

Is this a GCC extension or is it standard behavior? I have never seen this in practice. If it's standard, I'll start using it whenever possible.

Rifle answered 1/4, 2014 at 23:3 Comment(4)
new obviously doesn't create a const object. Why do you say that?Danais
new doesn't create a const object Why do you say that?Danais
Before Luchian had his revelation, in the case where T is a non-const-qualified type and you write const T *ptr = new T;, then new obviously doesn't create a const object :-) It's legal to cast const away from ptr and use the result to modify the object in that case.Coleridge
@SteveJessop: Sure, but that has nothing to do with new. It's like int obj = 2; const int* ptr = &obj; *const_cast<int*>(ptr) = 3; I don't see anything surprising here.Danais
D
11

const is part of the type. It doesn't matter whether you allocate your object with dynamic, static or automatic storage duration. It's still const. Casting away that constness and mutating the object would still be an undefined operation.

constness is an abstraction that the type system gives us to implement safety around non-mutable objects; it does so in large part to aid us in interaction with read-only memory, but that does not mean that its semantics are restricted to such memory. Indeed, C++ doesn't even know what is and isn't read-only memory.

As well as this being derivable from all the usual rules, with no exception [lol] made for dynamically-allocated objects, the standards mention this explicitly (albeit in a note):

[C++03: 5.3.4/1]: The new-expression attempts to create an object of the type-id (8.1) or new-type-id to which it is applied. The type of that object is the allocated type. This type shall be a complete object type, but not an abstract class type or array thereof (1.8, 3.9, 10.4). [Note: because references are not objects, references cannot be created by new-expressions. ] [Note: the type-id may be a cv-qualified type, in which case the object created by the new-expression has a cv-qualified type. ] [..]

[C++11: 5.3.4/1]: The new-expression attempts to create an object of the type-id (8.1) or new-type-id to which it is applied. The type of that object is the allocated type. This type shall be a complete object type, but not an abstract class type or array thereof (1.8, 3.9, 10.4). It is implementation-defined whether over-aligned types are supported (3.11). [ Note: because references are not objects, references cannot be created by new-expressions. —end note ] [ Note: the type-id may be a cv-qualified type, in which case the object created by the new-expression has a cv-qualified type. —end note ] [..]

There's also a usage example given in [C++11: 7.1.6.1/4].

Not sure what else you expected. I can't say I've ever done this myself, but I don't see any particular reason not to. There's probably some tech sociologist who can tell you statistics on how rarely we dynamically allocate something only to treat it as non-mutable.

Danais answered 1/4, 2014 at 23:8 Comment(31)
I've just never seen a new const ... anywhere (but I guess I knew tbh, just thought it was interesting)Rifle
@LuchianGrigore: I can't say I've ever used it myself, but I don't see why it should be treated as any less valid than a const object created "on the stack", as it were.Danais
There is also example on 7.1.6.1/4Julianejuliann
C++03 5.3.4 §1 is exactly the same as C++11's with only difference that C++03 doesn't say anything about "over-aligned types".Raffle
Is this valid? int x = 0; const int *px = new (&x) const int(0); x = 1; cout << *px; ?Lenes
@JohannesSchaub-litb: No, I don't think so. It's masking an effective const_cast. One min. (It compiles, at least)Danais
Well, I don't know the answer.. I don't even know whether *px reads from an int or const int object.Lenes
@JohannesSchaub-litb: That's the question innitDanais
@JohannesSchaub-litb: I think that x = 1; is undefined behavior, because you're using an object that no longer exists (its storage has been reused). And naturally you have to destroy *px and use placement new to put an object of the original type, int, back before it goes out of scope.Muirhead
@BenVoigt: I think [C++11: 3.8/1] agrees with you (in that it says the object lifetime ends when the storage is "reused or released"). Ah, but, then again, [C++11: 3.8/7] disagrees. The name x now refers to the new object, not the old one that died.Danais
@LightnessRacesinOrbit: I know about that rule, that's what prompted my comment. The only thing I'm not sure of is whether x = 1; creates a new int object in the location, reusing the storage where the const int object briefly was. Objects with trivial constructors can be brought into existence just by using them. But ISTR reusing the storage of a const object is also forbidden...Muirhead
@BenVoigt: I don't think assignment could ever be considered to create a new object. Other than a temporary, that is.Danais
@BenVoigt: Yeah, 3.8/7 says the type of the original cannot be const-qualified (which, here, it isn't, of course). This is actually quite interesting; I think, after all this, it does mask an implicit const_cast and subsequent UB.Danais
Oh, but 3.8p7 explicitly allows this case. "If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, ... the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if: ... the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and the type of the original object is not const-qualified..."Muirhead
@BenVoigt: I just said all that ;)Danais
@LightnessRacesinOrbit: I think a read a version of your comment which mentioned 3.8p1 but not 3.8p7. Was there one?Muirhead
@BenVoigt: The one that mentions 3.8/1 originally stopped before "Ah, but then again", but that was some time ago and before all the subsequent comments :) I'm going to ask and self-answer this question tomorrow, if you two don't beat me to it. I'm sure I have it cracked and I think it's interesting.Danais
3.8p9 is somewhat relevant: "Creating a new object at the storage location that a const object with static, thread, or automatic storage duration occupies or, at the storage location that such a const object used to occupy before its lifetime ended results in undefined behavior." and then there's 3.8p1 "The lifetime of an object of type T begins when storage with the proper alignment and size for type T is obtained, and if the object has non-trivial initialization, its initialization is complete."Muirhead
@BenVoigt: That scenario is the inverse of this scenario, isn't it?Danais
Here's the solution: x indeed refers to the new, const object, per 3.8p7. And then 7.1.6.1p4 "any attempt to modify a const object during its lifetime (3.8) results in undefined behavior"Muirhead
@LightnessRacesinOrbit: I see people mentioning the example in 7.1.6.1p4, but not the rule. The rule doesn't actually require a const_cast to precede undefined behavior.Muirhead
@BenVoigt: No, course not. Modifying a const object is UB. Doesn't matter how you got around the compiler safeties to do it. unions would be another way to do it (though in asserting that such a technique wouldn't already be inherently undefined, I'm assuming that [say] a const int and an int are layout-compatible, of which I'm uncertain). Fair play for making it explicit, though: you're certainly correct in that nobody had directly cited that.Danais
@LightnessRacesinOrbit: Within the constructor during construction of a const object, *this is not const-qualified, right? And it can be safely used to mutate the object (its lifetime hasn't yet begun). But this can also be saved off into a non-const pointer... another way to get to UB without using const_cast explicitly.Muirhead
@BenVoigt: Yeah (12.1/4), yeah (7.1.6.1/4, 3.8) and yeah.Danais
@LightnessRacesinOrbit: Saving this before the object becomes const sounds like a very subtle way to subvert the type system. Wonder if one can find an environment where it is expressed as a runtime failure (as opposed to merely UB which is potential failure). That would be a good prank, I think.Muirhead
https://mcmap.net/q/823218/-is-it-legal-to-modify-a-dynamically-allocated-const-object-through-a-re-used-non-const-name/560648 (covers the basics). Leaving the prank for 1st April 2015 :)Danais
@BenVoigt I think that x = 1; is undefined behavior, because you're using an object that no longer exists. Doesn't it create the object by the write again (as an int object), like it does when saying *(int*)malloc(sizeof(int)) = 0?Lenes
Johannes, doing that would be illegal, you aren't allowed to create an new object that overlaps a const object, during the lifetime of the const object. But no there isn't an int recreated, because the name x now refers to the new const object.Muirhead
@BenVoigt "you aren't allowed to create a new object that overlaps a const object"? Is that the case here? Why doesn't the lifetime of the const int object end when its storage is reused?Lenes
3.8p9 talks about storage that used to be occupied by a const object.Muirhead
@JohannesSchaub-litb: Perhaps let's take this up on the new question I raised for this issue. Besides, all quotes relevant to your doubts are covered both there and in comments above.Danais
W
17

new obviously doesn't create a const object (I hope).

If you ask new to create a const object, you get a const object.

there's no reason you can't legally const_cast the constness away and actually modify the object.

There is. The reason is that the language specification calls that out explicitly as undefined behaviour. So, in a way, you can, but that means pretty much nothing.

I don't know what you expected from this, but if you thought the issue was one of allocating in readonly memory or not, that's far from the point. That doesn't matter. A compiler can assume such an object can't change and optimise accordingly and you end up with unexpected results.

Wardlaw answered 1/4, 2014 at 23:13 Comment(12)
+1 for pointing out that because you can do something doesn't mean that you should do it.Raffle
so const X* x = new X is a pointer to a const object? Even though new X doesn't create a new object ?Rifle
Just to be clear, you are allowed to delete that const pointer right? Otherwise, how do you get rid of it?Sonstrom
@Luchian that's equivalent to X* tmp = new X; const X* x = tmp;, so yeah, that you can const_cast and mutate.Wardlaw
@Mysticial: I'd say that delete cleans up the memory regardless what occupies it. AFAIK the type is used just to determine the way the object should be cleaned (destructor) and to determine the size of the memory block that is about to be freed.Raffle
@Sonstrom destructors can run normally on any const objects, and the deallocation functions (i.e. operator delete) don't deal with objects (only untyped pointers to raw memory), so there's no issue.Wardlaw
@Mysticial: Why wouldn't you? By extension, do you think that const A a; can never go out of scope?Danais
@LuchianGrigore, if you create a non-const object and then assign it to a const object pointer, I believe you're able to const_cast it back to a non-const pointer and modify it. But I'm agreeing with this answer, you've created a const object and so it must remain.Troglodyte
@MarkRansom my initial statement was related to the answer I linked, which was about creating a non-const object, that's what I was referring to when I talked about const_cast.Rifle
@LuchianGrigore: I have commented on the answer and downvoted accordingly. Unfortunately, it was a mis-answer by our esteemed colleague Vlad.Danais
Whether you can legally do it depends on your definition of legally. If it means: it compiles, because that's what const_cast is for - then yes, it is legal. If it means: does it comply with the standard, the answer is no. In any case, I think that programmers should file a document justifying each use of const_cast and should be judged by a special committee for this. If you declare something as const, it's telling the compiler you will treat it as such; IMO const_cast is like saying "Oops, I made up my mind, please forget what I told you earlier".Teresiateresina
@Teresiateresina No, that's flat-out wrong. const_cast is not like saying that: just read my last paragraph. It's not like saying that because the compiler won't forget what you told it earlier (e.g. coliru.stacked-crooked.com/a/5ad38eabb96ea451). Please stop abusing const_cast.Wardlaw
D
11

const is part of the type. It doesn't matter whether you allocate your object with dynamic, static or automatic storage duration. It's still const. Casting away that constness and mutating the object would still be an undefined operation.

constness is an abstraction that the type system gives us to implement safety around non-mutable objects; it does so in large part to aid us in interaction with read-only memory, but that does not mean that its semantics are restricted to such memory. Indeed, C++ doesn't even know what is and isn't read-only memory.

As well as this being derivable from all the usual rules, with no exception [lol] made for dynamically-allocated objects, the standards mention this explicitly (albeit in a note):

[C++03: 5.3.4/1]: The new-expression attempts to create an object of the type-id (8.1) or new-type-id to which it is applied. The type of that object is the allocated type. This type shall be a complete object type, but not an abstract class type or array thereof (1.8, 3.9, 10.4). [Note: because references are not objects, references cannot be created by new-expressions. ] [Note: the type-id may be a cv-qualified type, in which case the object created by the new-expression has a cv-qualified type. ] [..]

[C++11: 5.3.4/1]: The new-expression attempts to create an object of the type-id (8.1) or new-type-id to which it is applied. The type of that object is the allocated type. This type shall be a complete object type, but not an abstract class type or array thereof (1.8, 3.9, 10.4). It is implementation-defined whether over-aligned types are supported (3.11). [ Note: because references are not objects, references cannot be created by new-expressions. —end note ] [ Note: the type-id may be a cv-qualified type, in which case the object created by the new-expression has a cv-qualified type. —end note ] [..]

There's also a usage example given in [C++11: 7.1.6.1/4].

Not sure what else you expected. I can't say I've ever done this myself, but I don't see any particular reason not to. There's probably some tech sociologist who can tell you statistics on how rarely we dynamically allocate something only to treat it as non-mutable.

Danais answered 1/4, 2014 at 23:8 Comment(31)
I've just never seen a new const ... anywhere (but I guess I knew tbh, just thought it was interesting)Rifle
@LuchianGrigore: I can't say I've ever used it myself, but I don't see why it should be treated as any less valid than a const object created "on the stack", as it were.Danais
There is also example on 7.1.6.1/4Julianejuliann
C++03 5.3.4 §1 is exactly the same as C++11's with only difference that C++03 doesn't say anything about "over-aligned types".Raffle
Is this valid? int x = 0; const int *px = new (&x) const int(0); x = 1; cout << *px; ?Lenes
@JohannesSchaub-litb: No, I don't think so. It's masking an effective const_cast. One min. (It compiles, at least)Danais
Well, I don't know the answer.. I don't even know whether *px reads from an int or const int object.Lenes
@JohannesSchaub-litb: That's the question innitDanais
@JohannesSchaub-litb: I think that x = 1; is undefined behavior, because you're using an object that no longer exists (its storage has been reused). And naturally you have to destroy *px and use placement new to put an object of the original type, int, back before it goes out of scope.Muirhead
@BenVoigt: I think [C++11: 3.8/1] agrees with you (in that it says the object lifetime ends when the storage is "reused or released"). Ah, but, then again, [C++11: 3.8/7] disagrees. The name x now refers to the new object, not the old one that died.Danais
@LightnessRacesinOrbit: I know about that rule, that's what prompted my comment. The only thing I'm not sure of is whether x = 1; creates a new int object in the location, reusing the storage where the const int object briefly was. Objects with trivial constructors can be brought into existence just by using them. But ISTR reusing the storage of a const object is also forbidden...Muirhead
@BenVoigt: I don't think assignment could ever be considered to create a new object. Other than a temporary, that is.Danais
@BenVoigt: Yeah, 3.8/7 says the type of the original cannot be const-qualified (which, here, it isn't, of course). This is actually quite interesting; I think, after all this, it does mask an implicit const_cast and subsequent UB.Danais
Oh, but 3.8p7 explicitly allows this case. "If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, ... the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if: ... the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and the type of the original object is not const-qualified..."Muirhead
@BenVoigt: I just said all that ;)Danais
@LightnessRacesinOrbit: I think a read a version of your comment which mentioned 3.8p1 but not 3.8p7. Was there one?Muirhead
@BenVoigt: The one that mentions 3.8/1 originally stopped before "Ah, but then again", but that was some time ago and before all the subsequent comments :) I'm going to ask and self-answer this question tomorrow, if you two don't beat me to it. I'm sure I have it cracked and I think it's interesting.Danais
3.8p9 is somewhat relevant: "Creating a new object at the storage location that a const object with static, thread, or automatic storage duration occupies or, at the storage location that such a const object used to occupy before its lifetime ended results in undefined behavior." and then there's 3.8p1 "The lifetime of an object of type T begins when storage with the proper alignment and size for type T is obtained, and if the object has non-trivial initialization, its initialization is complete."Muirhead
@BenVoigt: That scenario is the inverse of this scenario, isn't it?Danais
Here's the solution: x indeed refers to the new, const object, per 3.8p7. And then 7.1.6.1p4 "any attempt to modify a const object during its lifetime (3.8) results in undefined behavior"Muirhead
@LightnessRacesinOrbit: I see people mentioning the example in 7.1.6.1p4, but not the rule. The rule doesn't actually require a const_cast to precede undefined behavior.Muirhead
@BenVoigt: No, course not. Modifying a const object is UB. Doesn't matter how you got around the compiler safeties to do it. unions would be another way to do it (though in asserting that such a technique wouldn't already be inherently undefined, I'm assuming that [say] a const int and an int are layout-compatible, of which I'm uncertain). Fair play for making it explicit, though: you're certainly correct in that nobody had directly cited that.Danais
@LightnessRacesinOrbit: Within the constructor during construction of a const object, *this is not const-qualified, right? And it can be safely used to mutate the object (its lifetime hasn't yet begun). But this can also be saved off into a non-const pointer... another way to get to UB without using const_cast explicitly.Muirhead
@BenVoigt: Yeah (12.1/4), yeah (7.1.6.1/4, 3.8) and yeah.Danais
@LightnessRacesinOrbit: Saving this before the object becomes const sounds like a very subtle way to subvert the type system. Wonder if one can find an environment where it is expressed as a runtime failure (as opposed to merely UB which is potential failure). That would be a good prank, I think.Muirhead
https://mcmap.net/q/823218/-is-it-legal-to-modify-a-dynamically-allocated-const-object-through-a-re-used-non-const-name/560648 (covers the basics). Leaving the prank for 1st April 2015 :)Danais
@BenVoigt I think that x = 1; is undefined behavior, because you're using an object that no longer exists. Doesn't it create the object by the write again (as an int object), like it does when saying *(int*)malloc(sizeof(int)) = 0?Lenes
Johannes, doing that would be illegal, you aren't allowed to create an new object that overlaps a const object, during the lifetime of the const object. But no there isn't an int recreated, because the name x now refers to the new const object.Muirhead
@BenVoigt "you aren't allowed to create a new object that overlaps a const object"? Is that the case here? Why doesn't the lifetime of the const int object end when its storage is reused?Lenes
3.8p9 talks about storage that used to be occupied by a const object.Muirhead
@JohannesSchaub-litb: Perhaps let's take this up on the new question I raised for this issue. Besides, all quotes relevant to your doubts are covered both there and in comments above.Danais
F
0

My way of looking at this is:

  • X and const X and pointers to them are distinct types
  • there is an implicit conversion from X* to const X*, but not the other way around
  • therefore the following are legal and the x in each case has identical type and behaviour

    const X* x = new X; const X* x = new const X;

The only remaining question is whether a different allocator might be called in the second case (perhaps in read only memory). The answer is no, there is no such provision in the standard.

Fluxmeter answered 5/4, 2014 at 23:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.