Intolerance, but only for nullptrs!

I’m in the camp that believes in being clear about what the code communicates: if it’s supposed to fail, it should fail.

Hence, I went forward with a piece of code that simply quits the play session if a null pointer is detected. It even logs out the function and line number where it’s detected. Currently, it only works in a context which can provide a reference to a valid UWorld* pointer - so in AActor and UActorComponent classes for now.

// Put this in a .h file somewhere.

#include "Kismet/KismetSystemLibrary.h"

#define GUARD(ptr, world) \
    if ((ptr) == nullptr)\
	{\
		UE_LOG(LogTemp, Error, TEXT("nullptr at %s: %d"), TEXT(__FUNCTION__), __LINE__);\
		UKismetSystemLibrary::QuitGame((world), (world)->GetFirstPlayerController(), EQuitPreference::Quit, false);\
	}

Inside an UActorComponent's BeginPlay function, use it like:

// Called when the game starts
void UDoorOpener::BeginPlay()
{
	Super::BeginPlay();

	GUARD(Trigger, GetWorld());
	GUARD(DoorOpenInstigator, GetWorld());
...
}

Be careful since GetWorld() returns nullptr in the constructor! Use it only when you know for sure that a valid UWorld* pointer is available in that context - according to its definition, it will also be nullptr if a UActorComponent is not spawned in a level!

This of course won’t prevent crashes if there’s anything else that may break the game in a later piece of code.

1 Like

I’ve already found a case where this fails.

I tried to use a null pointer right after a GUARD() but immediately got a crash. It looks like The quitting routine is slow and launched async to the rest of the code, so the condition may always fail. I’ll change this macro to something which also takes in a block so that we can prevent running fatal code.

In a header file titled after my project name:

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "Kismet/KismetSystemLibrary.h"

// Guard a null pointer, issuing a quit if null. Use `block` to run code securely.
#define GUARD(ptr, world, block)                                                                                    \
    if (!(ptr))                                                                                                     \
    {                                                                                                               \
        UE_LOG(LogTemp, Error, TEXT("nullptr at %s: %d"), TEXT(__FUNCTION__), __LINE__);                            \
        UKismetSystemLibrary::QuitGame((world), (world)->GetFirstPlayerController(), EQuitPreference::Quit, false); \
    }                                                                                                               \
    else                                                                                                            \
    {                                                                                                               \
        block                                                                                                       \
    }

If it’s in the same function, I’ll have to run any unsafe code in the block portion.
So we get something like this for usage:

    PhysicsHandle = GetOwner()->FindComponentByClass<UPhysicsHandleComponent>();
    GUARD(PhysicsHandle, GetWorld(), {});

Where that empty scope can have anything I’d want to run while protecting my pointer, so that things don’t crash.

I’m confident I won’t need to protect the pointer anywhere else since the callbacks execute in a strictly deterministic fashion - only after all of BeginPlay() is done executing (or all the dispatches are made) does the Tick*() begin to work. Only in case I use the pointer right after the GUARD macro in BeginPlay() I run the risk of crashes.

No, it seems I was mistaken here as well. The execution of UKismetSystemLibrary::QuitGame() is much slower than the time taken for switching between BeginPlay and Tick* so once again an unsafe pointer crashes the game.

So I suppose I’ll have to use the macro at every invocation site at least once. In any case, if I use the block through the macro it will prevent any problems since it’s a simple if statement, and if it fails it’ll quit to the editor.

image

This proves that the Quit execution is quite slow, since these logs indicate the different call sites as well.

You might need to use the UE4 source code or may need to put it into the right class possibly having to override or extend a class or something.

I haven’t looked to see if a regular project includes the code that can do what you’re wanting to do but it won’t necessarily be so as UE4 has its own crash reporter. https://docs.sentry.io/platforms/native/guides/ue4/

Don’t know how that interacts with what you’re trying to do. But technically, you could just use the crash reporter Epic provides. Nulls are a quit anyway.

Quitting from a null isn’t necessarily the best choice though. Its fine for development but users don’t really like hard crashes. So doing enough polish and fixing all crashes is going to be better. Soft deal with nulls where possible. Nothing beats just fixing them in the first place before release.

1 Like

I agree, this is just a development crutch, I don’t think I’ll put it in release ever. After I use this macro, I usually try to deal with nullptrs throughout the code so that all references I use are valid.

I also like the idea of using Sentry but maybe it’s not necessary for the toy projects we make in the course.

The macro in this case also logs out the line number and function name where it encounters a nullptr so it’s ideal for development purposes only.

Thanks for your input! Learnt about Sentry - may turn out to be useful in the future!

Privacy & Terms