In this code snippet, why don't c++ compilers just return 1 when compiling test()
, but read the value from memory?
struct Test {
const int x = 1;
// Do not allow initializing x with a different value
Test() {}
};
int test(const Test& t) {
return t.x;
}
Compiler output:
test(Test const&): # @test(Test const&)
mov eax, dword ptr [rdi]
ret
I would have expected:
test(): # @test(Test const&)
mov eax, 1
ret
Is there any standard-compliant way to modify the value of Test::x
to contain a different value than 1
? Or would the compilers be allowed to do this optimization, but neither gcc nor clang have implemented it?
EDIT: Of course you immediately found my mistake in making this a minimum example, that is allowing aggregate initialization for the struct. I updated the code with an empty default constructor that prevents that. (Old code on godbolt)
Now you've disallowed using a constructor to create an instance of a Test
object with a different x
value, but gcc/clang still aren't optimizing.
It may be legal to use char*
or memcpy
to create an object-representation of a Test
object with a different x
value, without violating the strict-aliasing rule. That would make the optimization illegal.
Update, see discussion in comments; in the ISO standard 6.8.4 basic.type.qualifier "A const object is an object of type const T
" and doesn't rule out it being a sub-object, and getting at it via a pointer to the struct probably just counts as a non-const access path to a const object. (Any attempt to modify a const object during its lifetime results in undefined behavior doesn't leave room for loopholes since this is an object, not a reference to an object). So the char*
and memcpy
methods look to be UB, and even placement-new probably can't help: Placement new and assignment of class with const member - reuse is allowed only if "the type of the original object is not const-qualified".
(That language about not reusing the storage of a const
object changed in C++20; it now leaves the door open for using placement-new
on a whole struct/class object that's non-const
, even if it contains const
members.)
Manufacturing a brand new Test
object with arbitrary x
value via std::bit_cast<Test>( int )
still appears to be fully legal even in ISO C++. It is Trivially Copyable. Also, it appears that real implementations such as GCC and clang define the behaviour for all these cases, at least de-facto; I didn't check their official docs to see if it's mentioned as a GNU extension. As far as optimizer limits, that's what matters.
Test foo;
*(char*)&foo = 3; // change first byte of the object-representation
// which is where foo.x's value lives
In reference contexts in C++, const
means you can't modify this object through this reference. I don't know how that applies for a const
member of a non-const
object.
This is a Standard Layout type, so it should be binary compatible with an equivalent C struct, and also safe to write
/read
to a file and back without UB. It's a POD type (or I think the C++20 replacement for the concept of POD). It's even trivially copyable with or without a Test(const Test&) = default;
copy-constructor, although that's probably not relevant.
If it's legal to write it out to a file and read it back, it should still be well-defined even if the file is modified in between. Or if we memcpy
it to an array, modify the array, and copy back:
Test foo;
char buf[sizeof(foo)];
memcpy(buf, &foo, sizeof(foo));
buf[0] = 3; // on a little-endian system like x86, this is buf.x = 3; - the upper bytes stay 0
memcpy(&foo, buf, sizeof(foo));
The only questionable step is the final memcpy
back into foo
; this is what creates a Test
object with an x
value the constructor couldn't produce.
@Klauss raised a concern about overwriting the whole object without destructing it and doing a placement-new of the new one. I thought that was allowed for Standard Layout POD types, but I haven't checked the standard. That should be allowed for a struct or class whose members are all non-const
; that's the point of Standard Layout and POD / TrivialType. In any case, the char*
version avoids doing that, not rewriting the whole object.
Does merely having a const
member break the ability to write/read the object representation to a file? I don't think so; having a const
member doesn't disqualify a type from being Standard Layout, Trivial, and even Trivially Copyable. (This point is the biggest stretch; but I still think it's legal unless someone can show me in the standard where it isn't legal to poke around in the object-representation of a non-const class object.)
It would be extremely weird if having or not-having a constructor that allowed different initializers for the const int x
member was the difference between it being UB or not to write/read this object to a file and modify it. The inability to create a Test
object with a different x
value the "normal" way is a red herring as far as whether it's legal to poke around in the bytes of the object representation. (Although that is still a valid question for a class with a const
member.)
@Tobias also commented with an example (https://godbolt.org/z/3abaEqWdM) that uses C++20 std::bit_cast
to manufacture a Test
object with x == 2
that's constexpr-safe and evaluates correctly even inside a static_assert
. std::bit_cast
We can also see from this example that GCC and clang leave room for non-inline function calls to modify that member of an already-constructed Test
object:
void ext(void*); // might do anything to the pointed-to memory
int test() {
Test foo; // construct with x=1
ext (&foo);
return foo.x; // with ext() commented out, mov eax, 1
}
# GCC11.2 -O3. clang is basically equivalent.
test():
sub rsp, 24 # stack alignment + wasted 16 bytes
lea rdi, [rsp+12]
mov DWORD PTR [rsp+12], 1 # construct with x=1
call ext(void*)
mov eax, DWORD PTR [rsp+12] # reload from memory, not mov eax, 1
add rsp, 24
ret
It may or may not be a missed optimization. Many missed-optimizations are things compilers don't look for because it would be computationally expensive (even an ahead-of-time compiler can't use exponential-time algorithms carelessly on potentially-large functions).
This doesn't seem too expensive to look for, though, just checking if a constructor default has no way to be overridden. Although it seems lowish in value in terms of making faster / smaller code since hopefully most code won't do this.
It's certainly a sub-optimal way to write code, because you're wasting space in each instance of the class holding this constant. So hopefully it doesn't appear often in real code-bases. static constexpr
is idiomatic and much better than a const
per-instance member object if you intentionally have a per-class constant.
However, constant-propagation can be very valuable, so even if it only happens rarely, it can open up major optimizations in the cases it does.