A few C++ tips

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 a const value. In other words, dereferencing the pointer yields const int, but the pointer itself can be mutated to point to some other value const int:
    • pa = &b is valid (pointer can be modified to point to a different thing)
    • *pa = 2 is invalid (pointer dereferences to const int)
  • const int * const pb is a const pointer to a const value. Dereferencing it yields const int, and the pointer itself cannot be mutated.
    • pb = &a is invalid (pointer is a const, cannot be changed)
    • pb = 2 is invalid (pointer dereferences to const 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

Your email address will not be published. Required fields are marked *