Why is `UObject` layered into `UObjectBaseUtility` and `UObjectBase`?

UObject is derived from UObjectBaseUtility and UObjectBaseUtility is derived from UObjectBase:

class COREUOBJECT_API UObject : public UObjectBaseUtility { /*...*/ }
class COREUOBJECT_API UObjectBaseUtility : public UObjectBase { /*...*/ }
class COREUOBJECT_API UObjectBase { /*...*/ }

But the size of UObject and UObjectBase is the same:

static_assert(sizeof(UObject) == sizeof(UObjectBase)); // true

Which means that neither UObject or UObjectBaseUtility add any state over their bases, all the state is stored in UObjectBase.

Then what’s the purpose of UObjectBaseUtility and UObjectBase ? Why wasn’t the functionality just implemented in UObject ? Why do UObjectBaseUtility and UObjectBase even exist?

(And relatedly, why does FUObjectItem have a pointer to UObjectBase and not UObject? Aren’t all UObjectBases base class subobjects of a UObject ? Or are there objects other than UObjects that derive from UObjectBase ? I couldn’t seem to find any.)

2 Likes

I have no idea how Epic landed on the 3 classes, hopefully someone comes along and pops in with a good answer.

But normally, the idea is separation of concerns. For instance, with SOLID - classes have a single responsibility. It’s much easier to isolate functionality for unit tests, etc. And, when you fix a bug it minimizes the code that has to be re-tested etc. Also, see God Class.

As for giving out pointers/references to UObjectBase vs UObject, that would be common when you don’t want that thing to have access to the bigger implementation. That might reduce risks of regressions, etc, and improve maintainability, etc. Like, “Okay, I’ll tell you this is a car, but I’m not telling you it’s a Toyota or you’re just going to add a bunch of dependencies we don’t need, like on metric-only bolts”.

But no clue what the particulars are in this case, other than it should be broken into smaller code units as opposed to grown into larger ones. Not exactly a guess, but not exactly a concrete answer for this particular case.

3 Likes

Thanks for the effort @Imakevideogames, but if you are correct about the motivation behind this layering - then I guess my question becomes: What are these different (seperated) concerns and the single responsibilies of each of UObject, UObjectBaseUtility and UObjectBase? What functionality of UObject is hidden by UObjectBaseUtility or UObjectBase, and who is it being hidden from, and to what end?

Eh, just looking at the code, … there’s a rough breakdown of low-level details, garbage-collection, and object lifecycle management (in UObjectBase, UObjectBaseUtility, and UObject, respectively). roughly.

Also, UObjectBase depends on very little of the rest of the engine, it’s very basic and minimal stuff. UObject depends on a lot of the rest of the engine, etc does a lot.

Not sure about Epic’s motivations here, but there’s lots of cases where something like UObject might be inherently not-unit-testable because it requires too much support from the engine. But, the basic low level stuff might be totally unit testable. So, the latter goes in UObjectBase and gets rigorously Unit Tested, but the stuff in UObject is tested by other means - functional testing, integration testing, etc. Things like that are fairly common.

2 Likes

I assume this is hypothetical - the only tests I could find for CoreUObject are under Engine/Source/Runtime/CoreUObject/Private/Tests:

ObjectHandleTest.cpp
ObjectPtrTest.cpp
ObjectRefTrackingTestBase.cpp
ObjectRefTrackingTestBase.h
Serialization/BulkDataTests.cpp
Serialization/EditorBulkDataTests.cpp

and they make no mention of UObjectBase. (Unless I’ve missed something?)

  • As a general statement: not hypothetical
  • Applied to UE and UObject specifically: extremely hypothetical and almost certainly not true

(Epic probably does have private unit tests tho, as part of their build pipeline… code we don’t ever see. At least, I’d hope they do.)

However… The statement is true here. The code is bifurcated:

  • UObject-derived code that is not unit-testable
  • other code that IS unit testable

In this case the unit tests are running outside of UE. UObjects are pretty much impossible to make work outside of the engine. But, a lot of code CAN be made to work outside of UE. So, in this case all code that’s critical to unit test does not go on UObjects. The base code is unit-tested; the UObject subclasses code are not… Even if it means splitting some thing into multiple parts just to unit test the important stuff.

I don’t recommend it, but some of the mad ramblings about running UE modules outside the engine are captured here:

^ Madness, really, but there were reasons for pursuing it :rofl: