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 * pais a non-constpointer to aconstvalue. In other words, dereferencing the pointer yieldsconst int, but the pointer itself can be mutated to point to some other valueconst int:pa = &bis valid (pointer can be modified to point to a different thing)*pa = 2is invalid (pointer dereferences toconst int)
const int * const pbis aconstpointer to aconstvalue. Dereferencing it yieldsconst int, and the pointer itself cannot be mutated.pb = &ais invalid (pointer is aconst, cannot be changed)pb = 2is 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
You must be logged in to post a comment.