C++ Move Semantics in Unreal Engine 4

An introduction into C++ move semantics and how they can be utilized when developing games with Unreal Engine 4
Published on 5/15/2020
info This blog post is loosely based on a talk Manuel Wagner and I held at the Remote Control Production Devsummit 12 back in December 2019.

I’ll cover the basics first, so if you’re already familiar with move semantics and value categories and only want to know, how using move semantics in Unreal Engine 4 differs from standard C++, you can skip the majority of the blog post and go straight to UE4 Specifics.

The Copy Problem

Let’s adress the most fundamental question first: What are move semantics and why do we need them? In traditional C++98 programming you have two ways in which objects are created: construction “from scratch” and copying of existing objects.

// Default constructor (= create instance from scratch)
FFoo();
// Specialized constructor from data (= create instance from arbitrary data)
FFoo(int, char*);
// Copy constructor (= make copies of existing Foo instances)
FFoo(const FFoo& f);
// Copy assignment operator (= make a copy by assigning an existing instance)
void operator=(const FFoo& f);​

Copying an object is always connected to a processing and more importantly memory/resource allocation cost. To circumvent this, C++11 added the concept of moving objects, which allows transfering dynamic resources of old instances to new ones under the condition that the old instance is not used anymore after transferral.

The move functionality is declared like this:

// Move constructor (create new Foo by reusing resources from f)
FFoo(FFoo&& f);​
// Move assignment (create new Foo by reusing resources from f)
void operator=(FFoo&& OtherFoo);​

Theoretically moving from an object gives us a performance benefit, because memory copies and resource allocations may be skipped.

To Move or to Copy

Before we jump to the implementation details, we should quickly discuss, when you should actually take the effort of doing so. I’m adressing this first, because I was personally implementing move constructors and moving functions before I properly understood them. Two of my biggest misconceptions around move semantics were:

  1. “I have to declare move constructors for all my types”
  2. “I have to explicitly write std::move() a lot, otherwise I don’t benefit from move semantics”

Let’s look at number 1 first - number 2 is discussed in Implicit vs Explicit Moving. Deciding for which types to implement custom move constructors basically boils down to following the Rule of Three/Five/Zero1. Just ask yourself one question: Does your type deal with ownership? Or, alternatively: Does your type allocate and/or manage (dynamic) resources?

Examples of types for which this is true:

  • Smart pointers
  • Containers
  • Handles for external resources (e.g. hardware devices or network connections)
  • Resource managing objects using RAII2

Examples of types for which this is not true:

  • POD types
  • Types with smart pointer/container members

The main reason for this is that you get default implementations of copy constructor, assignment, move constructor, move assignment and destructor, unless you define any one of them. So as soon as you implement one of these functions, you must define the others or else they are implicitly deleted and your type is not copyable/movable anymore!

The default implementation of the move constructor is equal to calling std::move() on all members in order of declaration. So another good indicator for wether you need a custom move constructor is whether you can do something more special than just reimplementing this functionality which is obviously more error prone than just using the defaults.

Also good to note is that for POD types or POD type members, moving is the same as copying. Imagine the difference between adding instances of either of the following two types to a std::vector:

// when added, the pointer to the dynamic memory allocation of numbers can be copied to the new instance in the vector
// sizeof(Foo) = 24 with gcc on Win64
// no reallocation necessary = possible performance gain
struct Foo
{
    // dynamic vector with 1000 entries
    std::vector<int> numbers;
}

// When added, a full copy must be performed
// sizeof(Bar) = 4000 with gcc on Win64
// All 1000 bytes need to be copied
struct Bar
{
    int numbers[1000];
}

Note that when performing copies, both will behave more or less the same. The difference between the types when copying is that a vector of Bar will allocate all memory contiguously while a vector of Foo will have a contiguous list of Foo objects and then other places in memory where the contents of the various “numbers” members are stored. Ignoring memory fragmentation, this will result in the same total number of bytes required for storing and copying 1000 instances of either Foo or Bar.

Value Categories

Not all objects are movable. Even for types that support move semantics, not all values are movable. To distinguish such values, we have to look at value categories 3. In C++98 you could divide expressions into lvalues and rvalues:

  • lvalues are uniquely identifyable values/instances that could generally be found on the left side of expressions (e.g. named variables)
  • rvalues are anonymous values that only make sense on the right side of an expression (e.g. integer literals, objects constructed with new, etc.)

I find it easiest to remember the distinction with rvalues in mind, because code consisting of rvalues only doesn’t really make any sense - try imagining a hello world program with just literals and without calling any functions or referencing any named objects. C++11 expanded this concept and introduced the following tree of value categories:

As you can see in the image, the first property of values that is important for determining their value category is whether they have identity, i.e. whether they can be uniquely identified by name. Those values contain our classical lvalues, but also a new group called xvalues. Together they are generalized lvalues or short glvalues. When we look on the right side, our rvalues have been split into two types: xvalues and prvalues. Prvalues are pretty much our former rvalues, pure values like literals or newly constructed objects that are not yet bound to a named variable. The property they have in common with xvalues is that both value categories allow moving from them. Generally it makes sense to move data from an instance that is not bound to anything and cannot be referenecd later, xvalues are no different in this, except they allow you to reference an object by name and still move data from it.

Refer to this overview of the most common values and their value categories:

lvalues xvalues prvalues
Named variable Explicitly moved object Literal
Function name T&& function input Newly constructed object
Function result of T& type Function result of T&& type Function result of T or T* type

You can also easily confirm this and test which value category an expression has by using type traits in static asserts:

// xvalues:​
static_assert(std::is_rvalue_reference<decltype(X)>::value);​
// lvalues:​
static_assert(std::is_lvalue_reference<decltype(X)>::value);​
// prvalues:​
static_assert(!std::is_reference<decltype(X)>::value);

// just replace X with the thing you want to test, e.g.
decltype(FFoo::FooMemberFunction)​                   // to test name of member function
decltype(FFoo::FooMemberFunction())                 // to test result of static member function
decltype(std::declval<FFoo>().FooMemberFunction())  // to test result of non-static member function
decltype(int32(4))​                                  // to test integer literal 4

Don’t work yourself up if you don’t get what xvalues are on first read. Even their naming is not easily explained. Bjarne Stroustroup and company struggled with coming up with a name at all and settled on the prefix x “for the center, the unkown, the strange, the xpert only, or even [the] x-rated”4. So I hope, I could make the basics of it clear anyways.

Implicit vs Explicit Moving

This should all be a lot easier to understand with a little example, in which we will also see that most moving happens implicitly if a type supports moves. Moving from prvalues is super straightforward:

// Construct a new FFoo object (on the right side)
// The compiler knows it's not used anywhere except for the assignment,
// so we can move its contents immediately into f.
FFoo f = FFoo(3, 5);

// Store a function result returned by-value in a new dynamic list.
// In C++98 we'd get a copy here. C++11 allows us to implicitly move as
// we cannot reference the original function result, so we might as well reuse its resources.
std::vector<int32> list = bar();

In the first example, if FFoo did not support moving, a copy would take place. Compilers are allowed optimize unnecessary copies away in a process called copy elision5, but it’s helpful to keep in mind that for the most part moving happens implicitly in place of copies, which are also implicit in most circumstances.

What if we want to store the function result, do some other operations and then do the move explicitly? Here, xvalues and explicit moving come into play:

std::vector<int32> list = bar();
// do something else, e.g. modify list...
std::vector<int32> secondList = std::move(list);

The function std::move() turns the object into an xvalue (this is the explicit part) that can then be moved from implicitly. Afterwards the state of list is undefined and any operation except for assigning it leads to undefined behavior6.

RValue-References

If you want to write a function that only makes sense to be called with rvalues that you can then move data from (e.g. when writing a custom move constructor), you have to somehow reference the rvalue. For this, you can use rvalue-references T&&. In contrast to regular references T& that point to lvalues, rvalue-references point to rvalues. They can be used like this:

struct BBar
{
    void setFoo(FFoo&& f)
    {
        // move f into fooMember
        fooMember = std::move(f);
    }

    void setFoo2(FFoo&& f)
    {
        // If you're only interested in certain members of f, you can do that too:
        barMember = std::move(f.bar);
    }
}

// calling setFoo():
BBar b;
b.setFoo(FFoo());

Note that you must not just assign rvalue references, but must call std::move() to perform a move! As soon as an object has a name it is an lvalue and cannot be moved from anymore - even when the objects is an rvalue-reference! Assigning a value from rvalue reference does not move, but will cause an unwanted copy:

void Bar(FFoo&& f)​
{​
    // Probably not what you want!​
    FFoo f2 = f;​
    // Do this instead:
    FFoo f3 = std::move(f);
}

To decide when to use rvalue-reference parameters, follow CppCoreGuidelines rule F.157 “Prefer simple and conventional ways of passing information” and F.168. Usually it makes sense to follow this order of steps for “in” parameters:

  1. If cheap to copy pass by value
  2. Otherwise pass by reference to const
  3. If you want to optimize more (taking performance measurements into account!), add an additional overload with non-const rvalue reference

For templates in which you don’t know anything about the type, you can use T&& forwarding references and utility perfect forwarding via std::forward() as per CppCoreGuidelines rule F.199.

UE4 Specifics

Now to the bits that are a bit special in UE4 C++: For a lot of functions and types defined in the std library, Unreal Engine 4 has custom types and functions that replace the standard versions. This is partly due to legacy reasons, partly in order to ensure feature support across as many compilers as possible. In many cases the UE4 code even adds some functionality not present in the standard library:

C++ std:: library Unrea Engine 4 Functionality
std::move() MoveTempIfPossible() Move an object if possible, copy otherwise
MoveTemp() Move and object if possible, doesn’t compile otherwise
std::forward() Forward() Forward a T&& template argument to either copy or move
CopyTemp() Force a copy. Rarely used to be very explicit

UE4 also reimplements type traits for testing value categories:

// xvalues:​
static_assert(TIsRValueReferenceType<decltype(X)>::Value);​
// lvalues:​
static_assert(TIsLValueReferenceType<decltype(X)>::Value);​
// prvalues:​
static_assert(!TIsReferenceType<decltype(X)>::Value);​

Smart pointers are encouraged to be used with not blueprint exposed code whenever it adds clarity and simplifies object lifetime management. The Unreal equivalents to std smart pointers are:

C++ std:: library Unrea Engine 4
std::unique_ptr TUniquePtr
std::shared_ptr TSharedPtr
std::weak_ptr TWeakPtr

It’s worth noting that all of this is not usable in the following contexts:

  • UObjects cannot be moved or referenced by smart pointers as they are always referenced by pointer and managed by the garbage collector
  • Functions with rvalue-reference or smart pointer parameters are not Blueprint exposable
  • UStructs may define custom move functionality, but rarely benefit from it, because they ususally are plain data structs
  • UStructs may be stored with smart pointers, but when passing to blueprint, they must be copied by value or wrapped in a different blueprint exposed struct type, practically negating the benefit of smart pointers

Sources


  1. CppReference: Rule of Three↩︎

  2. CppReference: RAII↩︎

  3. CppReference: Value Categories↩︎

  4. Bjarne Stroustrup: “New” Value Terminology↩︎

  5. CppReference: Copy Elision↩︎

  6. CppReference: std::move()↩︎

  7. Cpp Core Guidelines F.15↩︎

  8. Cpp Core Guidelines F.16↩︎

  9. Cpp Core Guidelines F.19↩︎