Learned This Week
Overview
Finishing up the C++: Advanced Topics LinkedIn Learning course, this week was centered around more complex facets of C++ along with some of the Unreal foundational concepts. Smart Pointers seem like a great investment to make within projects in order to help avoid memory leaks, and Move Semantics are a very neat alternative to the traditional copy method for data manipulation.
This week I completed the final LinkedIn Learning course on my list, meaning from here on out it will be a lot more Unreal and game design-based courses coupled with the Unreal documentation. I'm excited to start getting my hands dirty, but I'm glad I've taken the time to learn the fundamentals and basics instead of just diving in head-first.
That being said, this week I got to write some basic C++ code for the manipulation of objects in Unreal, mapping some existing blueprint functions over to the C++ environment along with redefining a blueprint data struct inside C++ and migrating references (which was an absolute pain, by the way. Lesson learned, if you're ever going to possibly use a struct inside C++, define it in C++ and not blueprints because migrating data type references is a real hassle).
Key Points:
- Smart Pointers
- Should be used whenever possible instead of basic C pointers unless there is a specific reason not to, as they allow for better pointer manipulation and a stronger control over memory allocation. There are three types of smart pointers:
- Unique Pointers
- Type of smart pointer that cannot be copied. Useful for situations where you need a pointer but require that the pointer have a one-to-one relationship with itself and the resource it points to.
- Shared Pointers
- This type of smart pointer supports copying, and carries a reference counter with it. When a copy is made or destroyed, the counter increments or decrements accordingly.
- When the reference counter reaches 0, the memory allocated is freed and the pointer is destroyed.
- Weak Pointers
- A form of shared pointer which does not affect the reference counter of a shared pointer. Helpful for situations where you need a pointer which does not affect the lifetime of the resource it points to or when you are attempting to avoid circular referencing.
- Move Semantics
- C++ has a feature which allows you to move data from one location to another instead of copying it. This is extremely helpful when working with large class constructs where copying large quantities of data would be very costly. Instead, the data can be moved either temporarily or permanently in order to avoid copying.
- The C Preprocessor
- C++ passes its programs through the C preprocessor before sending the file to the compiler - as such, preprocessor directives can be defined. The most common example is the #include directive, which places the contents of the included file within the destination file before passing the file to the compiler.
- These directives can be used to define things such as macros, include guards, and more.
- UPROPERTY Variables
- Unreal has its own set of preprocessor directives, among them being UPROPERTY(), which lets Unreal know the variable defined afterwards should be used inside the engine. There are a number of qualifiers that can be passed to the UPROPERTY directive which let the engine know how the variable can be used and manipulated by different parts of its program. For example, defining a variable as (BlueprintReadOnly, VisibleInstanceOnly) will mean that variable is only ever to be read, not updated, by blueprint, and can only be accessed on instances of the main class (can't be viewed on the Class Defaults).
- UFUNCTIONs
- Another preprocessor directive, UFUNCTION() works similarly to UPROPERTY and has its own set of qualifiers that can be passed. One of the most interesting is BlueprintCallable, which allows the function to be called by the blueprint graph. BlueprintPure can also be specified, meaning the function will be marked as pure (does not cache its result and does not have exec-based code) when in the event graph.
- Mapping Blueprint Nodes to C++
- When coding in C++, it's sometimes difficult to figure out the C++ equivalent of a blueprint node - but there is a way to do so.
- First, find the node you want to call in C++. Hover over it in the blueprint graph and find its Target - that's the header file in C++ where the function is defined. Go to that header file and locate the function you need by using a search in the file. It's that simple!
- Unreal also needs to map its variable types to C++ - for basic data types it's simple, but for things like vectors and rotators, Unreal has specific FStructs for each in C++.
- More complex data types like actors or worlds (levels) have different ways of defining their references.
- Prefixes in C++ Unreal types stand for the following:
- U - UObject
- A - Actor
- T - Template
- F - Float (originally used to distinguish from typical math constructs, now used to define any custom data type)
- Blueprint Events From C++
- Events in blueprints can be defined in C++ and called by C++ code - then either implemented or overridden in blueprint. This is one of the ways to call blueprint code from C++.
- BlueprintImplementableEvents
- These events are declared and called in C++, but have no C++ implementation. They exist to be implemented in blueprints, and are useful if you need to "fire and forget".
- BlueprintNativeEvents
- Similarly to Implementable events, these events are declared in C++ but can have C++ code associated with them. The code will fire in C++ unless the function has been overridden in blueprints. The code from blueprints can also choose whether or not to fire the code in the super (C++).
Nitty-Gritty
- C++ Advanced Topics LinkedIn Course - Jump to Top
- Smart Pointers
- Why smart pointers
- Template-based pointers
- Allows for better pointer manipulation and control over what memory is allocated and when
- Valuable to help with memory allocation and helping to mitigate the possibility of memory leaks
- When the end-of-scope for the main function is reached, the pointers and objects are destroyed, and the objects' destructors are called
- Easy to use and more powerful than bare-c pointers
- Unique pointer
- Type of smart pointer that cannot be copied
- Implemented with std::unique_ptr<type> or std::make_unique<type> (recommended)
- The .reset() method is used to either destroy an object or replace it with a new one
- The std::move() function can be used to move pointers, but attempting to copy them will cause the program to fail to compile
- You can release the pointer from the control of the smart pointer object, and does not call the destructor (the pointer is now null, but the object still exists)
- You can't pass a unique pointer to a function, as the function call would attempt to make a copy of the pointer which is not permitted
- You can still pass it by reference to a function, just not as a copy
- Shared pointer
- Functions the same way as a unique pointer, but allows for copies to be made
- Provides limited garbage collection to clean up any objects being created and destroyed with the shared pointer
- Implemented with std::shared_ptr<type> or std::make_shared<type> (recommended)
- When the number of referenced objects reaches 0, the shared pointer is also destroyed and frees the memory taken up
- The .reset() method works the same way as with unique pointers
- A reference count is held and incremented/decremented with every copy made or reset
- This can be retrieved using the .use_count() method
- Weak pointer
- Special type of shared pointer which does not increase or decrease the shared pointer's reference count
- Useful for naming a pointer which should not affect the lifetime of the resource it points to
- Created from a shared pointer using std::weak_ptr<type>(shared_ptr)
- Cannot be de-referenced without obtaining the shared pointer from it
- Used when you may need to avoid a "circular reference", in which an object points to another object which also points back to it - with shared pointers, those cannot be destroyed without also destroying the other
- To get around this, one pointer can be a shared pointer while the other would be a weak pointer
- Using a custom deleter
- Custom deleters are functions which are called in order to both remove the pointer but also do additional actions like clearing up other dependent resources
- Custom deleters can be specified when using the std::shared_ptr function but cannot be done through the make_shared convenience function version
- The function specified will then be called instead of the default deleter when the reference count of a shared pointer is 0
- Choosing a smart pointer
- So when should you use each?
- For most purposes, a shared pointer will do what you need and is the most flexible of the three types of smart pointers
- Weak pointers are useful for when you need an optional value
- Unique pointers are useful when you need a 1:1 relationship between the pointer and the object pointed to
- Move Semantics
- What is move semantics
- When an object is passed to a function by value, a copy constructor needs to be called to create a new version of that object that has the scope of the function itself, which can be costly depending on the object's size
- Instead, we can specify a move constructor which simply moves the data (reassociating it) to the new object for the scope of the function
- Understanding lvalues and rvalues
- Any expression that can appear on the left-hand side of an assignment (=) is an lvalue, and any expression that can only appear on the right-hand side of an assignment is an rvalue
- So what can be an rvalue?
- A nameless or expiring value (i.e. a + b, which will evaluate to something before being operated on)
- A literal or pure value (i.e. 42, or anything returned from a function which is not a reference)
- These type categories can be moved
- Using std::move
- Included with the <utility> library
- Moves a value from its source location to a destination location without having to copy it
- When values are moved, the original location is vacated and any data at the destination location is replaced with the moved values
- This is a great way of swapping data between variables (create a temp variable, set its contents by using move to the destination location's existing data, move the source location's data to the destination, then move the temp variable's data to the source location)
- It seems like Unreal has its own implementation of this which I have not yet explored
- The move constructor
- A move constructor is required with any declared classes in order to make sure the data is moved correctly - otherwise, a copy constructor will be called instead if the std::move() is used on a declared class to create another object of that class
- Move constructors must be declared with a noexcept qualifier in order to avoid errors with an object in an unknown state
- The .reset() method should be declared for the class and called in order to make sure the rvalue is set to an acceptable state
- The move assignment operator
- In the same sense, you need to declare a move assignment operator overload in order for your class to support move assignments
- The implementation is normally the exact same as the move constructor
- The copy-and-swap idiom
- Copy-and-swap is a lot more efficient than a regular move assignment operator as it makes a copy of the existing data, then uses that copy to move the data into the lvalue
- Rule of five
- The rule of three (not five) is that if you implement a destructor, copy constructor, or a copy assignment operator overload for you class, then you should implement all three (because the default versions won't have the functionality you need as you are managing data)
- The rule of five extends the rule of three to include the copy assignment and move assignment operator overloads due to the same reason
- Lambda Functions
- Lambda syntax
- Lambda functions/expressions are anonymous (without a name) functions with the ability to refer to identifiers outside of its own scope
- Lambda functions are passed a "capture", normally a variable (as a reference if it needs to be operated on) in order to be able to use that variable in its scope
- Convenient for circumstances wherein you would otherwise use a functor or a function pointer but the code doesn't need to be reused
- An easy way to declare and use a temporary function
- Captures
- Captures are used in lambda functions in order to be able to access data outside of its scope
- Captured variables are passed inside of square brackets []
- There are a number of different ways to capture variables
- Specified variables
- By value [var]
- By reference [&var]
- Specified per-variable [var, &var2]
- Pull all within the scope of where the lambda function is
- All variables by value [=]
- All variables by reference [&]
- All variables by reference except specified by value [&, var]
- All variables by value except specified by reference [=, &var]
- The C Preprocessor
- About the preprocessor
- The preprocessor is the first step of compilation (run before actual program compilation)
- Macros can be defined to apply to the full program (i.e. #define ONE 1 would replace all instances of ONE with 1 before passing the program to the compiler)
- Macros can be defined with parameters as well
- Processing can also be conditional (see the conditional compilation section below)
- Pragmas can be used to provide implementation-specific parameters (though this should be used sparingly as they are normally not very portable)
- Statements are terminated by end-of-line
- Macros as constants
- Macros can be helpful to define as constants (typically declared in all upper-case), but are just text replacements
- Types are defined in a literal manner (i.e. 1 would be an int, "1" would be a string)
- Macros are literal meaning they are not strongly typed
- It's typically suggested to use a const variable in C++ instead of defining macros whenever possible
- Additionally, constexpr variables can be used in situations where you couldn't use a const variable but still need a constant (i.e. switch cases)
- Including files
- Inclusion is the most common and easily visible implementation of the preprocessor
- The include directive is replaced by the preprocessor with the contents of the referenced file before passing it to the compiler
- Header files enclosed within angle brackets <> denote system header files while header files enclosed within quotation marks "" denote files within a specific directory
- Conditional compilation
- You can check if a macro has been defined yet using #if defined or #ifdef (this can also be NOT by putting a ! before defined or using ifndef, for example: #if !defined MACRO or #ifndef MACRO)
- You can undefine macros using #undef
- If-else cascades are supported
- Defining macros
- Any valid code is permitted on the right-hand side of the macro because it's just a text replacement, so you can create macros with parameters as well (i.e. #define TIMES(a, b) (a * b) would replace any instance of TIMES(var1, var2) with (var1 * var2)
- This is typically not suggested to use as it is done before compilation, meaning any variation in the variables will not be processed by the compiler
- Including files only once
- When header files include other header files, the compiler can easily get confused as to what needs to be done, and will fail to build
- The solution to this is to put include guards in your header files by doing the following:

- An alternate solution is to use #pragma once at the beginning of your header file, which encapsulates the file with an include guard
- However, it is generally suggested to use the include guard directly as the pragma version can sometimes process incorrectly due to symbolic links or when include files are scattered across a file system
- Unit Tests
- The importance of unit tests
- Unit tests should be implemented in your programs in order to explore and check the multitude of different situations that your code may encounter to determine if bugs need to be ironed out
- Each unit test should check whether the taken action had the expected result
- Garbage in, garbage out - you need to set up your unit tests correctly in order to have any success in unit testing
- Course complete!

- Unreal Converting Blueprint to C++ Course - Jump to Top
- UPROPERTY: Exposing Variables
- Variables declared as UPROPERTYs in C++ are able to be accessed, read, or written to in blueprints depending on the flags they are given
- Variables that need to be accessed by both blueprint and C++ code should be implemented as accessible UPROPERTY variables as blueprint can easily access C++ data but C++ cannot easily access blueprint data
- Declaration of UPROPERTY variables is done by declaring a variable in the header file (a default value can also be given) and by placing a UPROPERTY() preprocessor directive above it like so:

- Typically, UPROPERTY variables are placed in either the private or protected sections
- Different access modifiers and information can also be passed to the directive, the most common being EditAnywhere and BlueprintReadWrite, the definitive list is below:

- The Blueprint column above refers to the ability to get and/or set the variable in the blueprint graph
- The Defaults column above refers to the class defaults section of a derived blueprint class
- The Instance column above refers to when the blueprint is placed in the world
- A Category can also be defined in the directive in order to define how it should show up in the class defaults - the final setup for the MaxGrabDistance looks like this:

- BlueprintCallable UFUNCTION
- The UFUNCTION preprocessor directive permits a specified function to be called within Unreal
- Including the BlueprintCallable specifier will allow the function to be called from within the blueprint graph
- Typically, UFUNCTIONs are implemented as either protected or public in order to be able to be called when needed
- Because we've already replaced the MaxGrabDistance needed for the following function with C++, we can also shift the function itself over to C++ as it is fairly straightforward and would probably run faster in C++ rather than in blueprint:

- The function in blueprint has no inputs and is pure, and the return value is a vector
- In order to begin to transfer the function over to C++, we need to first understand what types of variables map over to C++ from blueprint as - the fundamental types are below:

- The dashes above denote that the C++ type associated doesn't have a blueprint-implementable version
- Utility type variables have specific versions inside of C++ to allow them to be used within Unreal as shown below:

- Object and actor types also have C++ versions to add (as pointers):

- As the function we're going to be implementing isn't going to change any data, we can mark it as const-qualified and denote it as implementable as pure in blueprints like so:

- Once compiled, we'll get a blueprint compilation error because the C++ function has the same name and function signature, but we haven't specified that the parent (C++) function is virtual or able to be overridden
- However, because the parent call should be the same as the function has the same signature, all that needs to be done is to delete the blueprint version of the function and any references in the blueprint should resolve themselves as they will take on the C++ version instead
- Now we can get started on implementing the function in C++ instead - the blueprint function did its steps this way:
- Get the WorldRotation of self
- Get the forward vector of that rotator
- Multiply that value by the MaxGrabDistance
- Get the WorldLocation of self
- Add that value to the previous
- Return that summed value
- In C++, we can use similar functions to get the same data and return it using the same mathematical steps
- However, the GetForwardVector function is included in the Kismet Math Library which we can include by using #include "Kismet/KismetMathLibrary.h"
- The C++ code boils down to one line:

- Mapping Blueprint to C++
- There's an easy way to find the C++ equivalent of a specific system-defined Unreal blueprint node
- Hover over the Unreal blueprint node and find the Target, then go to the header file in C++ where the Target type is declared
- You can find this in Visual Studio by either jumping to a declaration of a text link that already exists in the file or by using Ctrl + , to use the "Go to All" feature in VS
- In that header file, you can search for the Unreal blueprint node name and you can find the associated C++ function
- Anything with prefixes like K2 often resolve to calling another function - if you jump to the definition, you can see what the correct function it calls is
- Kismet and K2 reference functions that are specifically meant to be compatible with blueprints, as Kismet was the predecessor system to the blueprint system that exists today
- Due to the ways that Unreal interacts with its actors and parts of the world, here are a few commonly included header files:

- We'll map two more functions over to C++ from blueprint as well - GetHoldLocation and GetPhysicsComponent
- GetHoldLocation

- This references a blueprint variable which we need to bring across to C++, which is the HoldLocation with a default value of 100, following the same process as MaxGrabDistance
- It follows a very similar process except it uses the HoldDistance rather than the MaxGrabDistance, so we can copy most of the code - the outcome looks like this:

- GetPhysicsComponent

- BlueprintImplementableEvent
- We can call implementations that exist in a blueprint from C++ code
- This is possible as long as the definition is included in C++ (think of virtual methods in a class, this concept is similar)
- One of the ways of doing this is to use a BlueprintImplementableEvent
- When declaring the UFUNCTION in C++, you should only declare it in the header file and it should not be defined in the .cpp file (because it will be implemented in the blueprint graph instead)
- BlueprintNativeEvent
- In contrast to a BlueprintImplementableEvent which cannot have C++ implementation and exists/needs to be overridden, BlueprintNativeEvents have a default C++ implementation which can be overridden if needed, and the blueprint implementation can call to the super (C++) implementation
- In order to output multiple variables from a single function, we use choose one variable to be the return value and the rest to be out parameters (pointer references passed to the function) like so:

- To define the C++ implementation, you need to use the _Implementation syntax in the function definition to let Unreal know that this is the C++ version of the function, like so:

- BlueprintNativeEvent functions can be overridden by using the Override dropdown in the Functions section of the blueprint
- Converting BP Structs to C++
- In the example project, there's a struct defined in blueprint named FQuestInfo, but we'd like to start using it in C++ as well, which means we need to shift its definition over into C++
- The easiest way to do this is to create a new C++ class that derives from Object, then to convert it into a UStruct
- Once the class has been created, we can change the class qualifier into a struct qualifier
- Then we remove the inheritance specifier and change the UCLASS() preprocessor directive into a USTRUCT() directive
- The U in the struct definition needs to be an F because it's a struct - the compiler will throw an error otherwise
- The final setup of this struct looks like this:

