4 ms·
This confuses me even more than C++ usually does. From the first answer: > struct foo{ int const x; }; > void some_func(foo*); > int bar() { > foo
by codeflo 5y ago
This confuses me even more than C++ usually does. From the first answer:
> struct foo{ int const x; };
> void some_func(foo*);
> int bar() {
> foo f { 123 };
> some_func(&f);
> return f.x;
> }
> bar will always return 123. The compiler may generate code that actually accesses the object. But the object model does not require this. f.x is a const object (not a reference/pointer to const), and therefore it cannot be changed.
I like to think I know quite a bit about C++, but I don’t get this at all. Why is f.x a const object even when accessed through a non-const variable? f isn’t const; there’s no const_cast here. At first glance, this looks like a bug in the spec, roughly like confusing
int const *p
(pointer to const) with
int const *const p
(const pointer to const). Only the latter will always point to the same thing.
What am I missing?
(Edit: Since it’s a bit hidden in the SO question, the idea is that some_func does something like this:
void some_func(foo *f) {
new(f) foo { 42 };
}
Note that there’s no const_cast here, and f’s storage isn’t const, so this isn’t UB to the best of my knowledge.
BTW, in case it’s not obvious, it’s not all that helpful to reply to the question why the spec is a certain way by tautologically repeating the spec.)
(Edit 2: Unfortunately also very typical for C++ discussions, I just love how people think that C++ confusing me must mean that I’m dumb, and then condescendingly post a wrong answer. Rule of thumb: If you’re confident in your ability to understand C++, you don’t understand it very well.)
- ptr 5y agoThe constiness of a pointer can be casted away while the constiness of a value variable cannot — it’s UB. foo::x is declared const, so it’s always const.
- codeflo 5y agoI realize that, but no variable is const, only the field is. And you don’t need a const_cast to change the value, as the example in the question shows, some_func can do: void some_func(foo *p) { new(p) foo { 42 }; } That’s not UB.
- bingo3131 5y agoIt is UB as the original object has non-static data members that are const-qualified.
- codeflo 5y agoI don’t think so. According to https://stackoverflow.com/a/39382728 https://stackoverflow.com/a/39382728 (the original SO answer that prompted the linked SO question, and which quotes the relevant parts of the spec), the allocation is not UB, but accessing the member after that (at least without the std::launder trick) is UB. It’s precisely this latter part that I don’t think is very reasonable to have in the spec.
- gpderetta 5y agoNote that accessing the new object via the pointer to placement new is perfectly fine, the issue is dereferencing the old pointer that logically point to an object whose lifetime has been terminated even if it mught actually contain the same bit pattern as the new pointer. I think this is related to the ill-defined concept of pointer provenance. Launder let you discard any provenance info for the old pointer and treat it as a new one. Tricky and very ill-defined, and according to the another comment elsethread this now doesn't require launder anymore (it probably was proven unworkable in practice).
- bingo3131 5y agoThe issue is that code outside of some_func does not know that f has been destroyed and a new object created in the same memory location, thus wouldn't know that the const member foo::x now has a new value. As for why the spec prohibits the modification of const values: one of the reasons for compilers being allowed to treat const values as being truly immutable is for performance. If the compiler can see the actual value then it can avoid the memory read entirely, and if it doesn't know the actual value then it only needs to do one memory read. If those const values could change without the compiler being aware then it now needs to do an awful lot more fetching from memory as it cannot guarantee that the value did not change. Some of it could be optimized away, but it would still make code a lot slower if - for example: mixing reads from const ints with pointer/reference manipulation of ints - as due to potential aliasing the compiler would likely have to read those const ints from memory each time.
- judofyr 5y ago> Why is f.x a const object even when accessed through a non-const variable? Because the member is const. It's a bit strange example because there's no reason to have mutable pointer to a struct with only const members. A better example would maybe be: struct foo{ int const x; int y; }; void some_func(foo *f) { foo.y = 0; } int bar() { foo f { 123, 456 }; some_func(&f); return f.x; // must always return 123 } EDIT: On second thought I'm also a bit confused. Wouldn't this be a "safe" way of modifying `f.x`? void some_func(foo *f) { *foo = foo { 5, 5 }; // probably wrong syntax? I don't write so much C/C++ }
- mikieng 5y agoRegarding your EDIT: that is not a valid way of modifying x. When you try to assign a new foo that way, what you are implicitly doing is calling the copy assignment operator. This normally works by copying each field from the input object to the destination object, but, since the field x is const, it cannot be re-assigned to the input object's x. In fact, very often the compiler will generate a copy assignment operator implicitly for you, but not in this case, because it can't copy values into a constant field. Also, since you have a local pointer f within some_func, so re-assigning a new object of type foo to it will not alter the original foo. This is not implied by your original comment but it's something one could wonder.
- codeflo 5y agoI think you have to do void some_func(foo *f) { new(f) foo { 5, 5 }; } because no assignment operator is generated for a struct with a const member.
- nyanpasu64 5y ago> no assignment operator is generated for a struct with a const member. I wanted to use const fields to enforce the logical invariant that a field never changes for the lifetime of an object, until the entire object is overwritten by a newly constructed object. Unfortunately C++ deleting the move-assignment operator means it's no longer practical to do so (though you can use placement-new for POD data where it's safe to not run the destructor, and even then it's obscure and will leak memory and possibly is UB as soon as you add an owning type). Instead, I keep a mutable field and have to remember to never mutate it when writing hundreds of lines of method implementations, and when I come back to the code months later and edit it, and someday I may slip up. Rust has no const fields at all.
- bingo3131 5y agofoo::x is declared const inside the struct in the example given. Because of this, regardles of how you access foo::x, it will always be const.
- deleted 5y ago[deleted]
- tsimionescu 5y agoNote that per some comments and modifications to older questions, the standard has actually moved to the sane world and no longer allows this "optimization" [1][2]. So std::launder is no longer required for this purpose - just the other ones. [0] https://stackoverflow.com/a/70419156 https://stackoverflow.com/a/70419156 (first part) [1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1971r0.html#RU007 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p197... [2] https://wg21.link/p1971r0 https://wg21.link/p1971r0
- gpderetta 5y agoThat's interesting thanks, although just from the delta it is not obvious how it fix things; I'll have to read it eith the full context. So is launder needed for anything now?
- tsimionescu 5y agoI will try to explain the logic as I understand it, and hopefully someone more knowledgeable will point out anyplace that I am wrong. The problematic code was this: struct X { const int n; }; union U { X x; float f; }; ... U u = {{ 1 }}; X *p = new (&u.x) X {2}; cout << u.x.n; //is this UB? After the placement new, we are in the case from this piece of the standard ("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"). u.x is "the name of the original object" - can we use it to manipulate the new object that placement new put in there? In the old standard, we can if "the type of the original object [...] does not contain any non-static data member whose type is const-qualified". Since X::n is const qualified, u.x can't be used to refer to the new object - enter std::launder. However, the new standard modifies this exception: we can still use u.x if "the original object is neither a complete object that is const-qualified nor a subobject of such an object". Neither u nor u.x are const-qualified objects, nor are they a sub-object of a const-qualified object. So, u.x now refers to the new object created by placement new. Still, std::launder seems to be needed for other cases, mostly related to reinterpret_cast. The C++ Reference site [0] shows these examples. [0] https://en.cppreference.com/w/cpp/utility/launder#Example https://en.cppreference.com/w/cpp/utility/launder#Example
- User23 5y ago> (Edit 2: Unfortunately also very typical for C++ discussions, I just love how people think that C++ confusing me must mean that I’m dumb, and then condescendingly post a wrong answer. Rule of thumb: If you’re confident in your ability to understand C++, you don’t understand it very well.) I sat next to a guy on the C++ Standards Committee and he was the first to admit that he didn't even fully understand C++. The abstract machine is monstrously complex.