const_cast
It is the case that const_cast
can remove or add const
ness to things. However, even if the const
ness is removed, if an object is declared to be const
in the first place, making any modifications to it remains undefined behaviour.
Consider this code
const int Value = 0;
int &ValueRef = const_cast<int &>(Value); // 1
ValueRef = 42; // 2
return Value;
Here we use const_cast
to strip away the const
from Value
and turn it into a int&
(1). We then attempt to assign a new value to it (2), and return it. However, the returned value will not be 42, it will remain 0.
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], 0
mov dword ptr [rbp - 8], 0
lea rax, [rbp - 8] ; &ValueRef = const_cast ...
mov qword ptr [rbp - 16], rax
mov rax, qword ptr [rbp - 16]
mov dword ptr [rax], 42 ; ValueRef = 42;
xor eax, eax ; eax <- 0
pop rbp
ret ; return eax // (eax == 0)
In summary, and as a general rule, you should never use const_cast
.
const
The const
keyword declares immutable thing; that much is clear, but it can be placed in surprising number of places. To start, consider a simple variable declarations.
int a = 10;
int b = 20;
const int * pa = &a;
const int * const pb = &b;
pa = &b;
pb = &a;
*pa = 2;
To start off, we have two plain int
variables a
and b
. We then declare two pointers sprinkled with the const
keyword:
const int * pa
is a non-const
pointer to aconst
value. In other words, dereferencing the pointer yieldsconst int
, but the pointer itself can be mutated to point to some other valueconst int
:pa = &b
is valid (pointer can be modified to point to a different thing)*pa = 2
is invalid (pointer dereferences toconst int
)
const int * const pb
is aconst
pointer to aconst
value. Dereferencing it yieldsconst int
, and the pointer itself cannot be mutated.pb = &a
is invalid (pointer is aconst
, cannot be changed)pb = 2
is invalid (pointer dereferences toconst int
)
In structs and classes, marking a method const
in effect adds const
to the implicit this
value. The complication you might encounter is having to implement two “getter-like” methods, where you’d need a non-const
as well as const
versions, but the method is not completely trivial, so you would not want to repeat the code.
struct S {
private:
int I = 0;
public:
int &Get() {
// much more complicated than 'return I'
return I;
}
const int &Get() const {
// much more complicated than 'return I'
return I;
}
};
You could implement the non-const version as
return const_cast<int&>(const_cast<const S*>(this)->Get());
but apart from being rather ugly, recall that const_cast
is evil. What you’re really doing is switching on the const
-ness of the this
pointer; what we can do is to define the GetCommon
function as a static method that receives the const
-appropriate pointer to “self”, and does the common algorithm to return the value. Note the use of decltype(auto)
, which is like perfect forwarding, but on the return side. It is in essence “return exactly the thing that the body returns”.
struct S {
private:
int I = 0;
template <typename Self>
static decltype(auto) GetCommon(Self *This) {
// much more complicated than 'return I'
return (This->I);
}
public:
decltype(auto) Get() { return GetCommon(this); }
decltype(auto) Get() const { return GetCommon(this); }
};
Note that you can use GetCommon(Self &This)
: a reference to This
, not a pointer; it changes nothing about the generated code or the semantics.
Casts and undefined behaviour
When using reinterpret_cast
to interpret some memory block as an address to an unrelated type, say a struct
, doing simple
struct FFoo {
int a;
float f;
};
alignas(FFoo) std::byte buff[sizeof(FFoo)];
FFoo *p = new(&buff) FFoo{};
int a = reinterpret_cast<FFoo*>(&buff)->a;
The compiler can optimise the result of reinterpret_cast
in a way that can lead to undefined behaviour when accessing the struct members (here, a
). To fix, wrap the cast in std::launder
.
int a = std::launder(reinterpret_cast<FFoo*>(&buff))->a;
Or, even better, use std::bit_cast
; in KOPI, a good example is
const uint64 Id = *std::bit_cast<uint64_t *>(UniqueNetId->GetBytes())
The UniqueNetId
value we know is a Steam-provided FUniqueNetId
, and we know that it contains a 64bit int. All we do is turn the opaque bytes into an uint64_t
.
Leave a Reply