Skip to main content

Week 3 - Classes, Inheritance, & Polymorphism

Learned This Week

Overview

Learning about classes in class! C++ is a language with extensive support for object-oriented programming, and classes are extremely flexible within it. Overloading, overriding, and multiple inheritance, oh my! Although unfortunately some features like constructor overloading aren't usable with Unreal, learning how C++ can utilize principles of OOP to a great extent was very helpful for me, and I expect it to be as I move into learning more with Unreal and C++.

This week, I completed the Programming Foundations: Object-Oriented Design LinkedIn Learning course along with the C++ for Python Programmers Runestone textbook. I delved further into OOP implementations in C++ with the C++ Advanced Topics LinkedIn Learning course and learned a little bit more about C++ in Unreal with the Converting Blueprint to C++ course offered by Unreal/Epic Games.

I was pretty excited to learn how flexible C++ is when it comes to classes, and was able to relate a lot of what I was learning to my knowledge of Unreal's blueprint system, as it mimics a lot of the same concepts. I feel like I now have a solid base knowledge of C++ that I can begin to apply to Unreal, which will be very helpful in the next few weeks as I begin taking more Unreal-centered courses.

Key Points:

  • Class Member Functions
    • Functions need to be const-qualified if they are going to be used by const objects. However, you can overload the same function with a const and a non-const version to allow the compiler to pick the appropriate function for the objects you are calling them with.
    • Special types of member functions called constructors and destructors can be specified and are called when an object of the class is created or destroyed. It's common to overload constructors to be applicable in multiple situations (though it's worth noting that in Unreal, classes only have one constructor as it is provided by and called by the engine).
    • The this keyword can be used to get a self-referencing pointer, which allows you to call other functions within the same object without having to worry about directly targeting the object itself.
    • Operators can be overloaded in addition to functions (see later for further information on function overloading). This allows you to force the compiler to interpret operator operations on your classes in a specific way - the most common overloads are the arithmetic operators for things like custom data class structures and members, but any operator can be overloaded (except the scope resolution, ternary, member, sizeof, and member pointer selector operators, as these are required for basic compiler tasks).
  • Inheritance
    • Inheritance situations fit into an "is a" relationship, for example, "A Cat is an Animal", in that a Cat class would derive from an Animal base class.
      • There are also other types of relationships between objects, Aggregation and Composition.
        • Aggregation
          • Collections of objects. Relationships are typically defined with a "has a" type, for example, "A Fleet has a Spaceship".
          • Aggregated objects are not destroyed when the overall aggregation is destroyed, but rather set free to exist without an aggregate.
        • Composition
          • A different type of object collection, where multiple components are "owned by" another, for example, "A Spaceship owns an Engine".
          • Composed objects are destroyed along with their compositional object. This is how Unreal sets up its collections within actors.
    • Within classes, there are three different levels of member (both data and function) access levels: public, protected, and private (private is the default and will be used unless specified otherwise)
      • Public
        • Available to all objects
      • Protected
        • Available to members of the same class (base and derived)
      • Private
        • Available to only the class that directly implements them
    • C++ has two types of inheritance: simple/single inheritance (where one class inherits directly from one other class) and complex/multiple inheritance (where one class inherits from multiple base classes).
    • Accessing members (data or function) on a derived class is easy if the access level for that member is protected or public - all the needs to be done is to use the name directly, as the compiler places the base class inside of the derived on compile time. However, it is much more common (for better readability and to avoid accidental future overloaded function calls) to use the name of the base class along with the scope resolution operator to tell the compiler specifically what function or data member you'd like to access, for example: Animal::name();
      • It's worth noting that this will still respect the access levels of the original class, i.e. you can't access private members via this method as they are still private to that original class
    • Access levels can be overridden by declaring another class as a "friend", which allows that class to directly access members within the specifier location that the friend class is specified. This is typically not recommended, as accessor functions should be used instead, but it is helpful if necessary.
  • Polymorphism
    • The concept of polymorphism in OOP centers around using multiple versions of the same function (overloading or overriding) to implement them in an easily accessible way, but still have each do the appropriate action for their relevant classes.
    • In C++, both overloading and overriding exist but overloading is much more common due to the way that C++ implements its function signatures.
    • While most of the overloading implementation was already covered in Week 2's post, I learned this week about the virtual qualifier. This qualifier (used in the base class) tells the compiler that the function may be overloaded in derived classes, and to look for that version if the function is called on a derived class.

Nitty-Gritty

Jump to:
  • C++ Advanced Topics LinkedIn Course - Jump to Top
    • Classes and Objects
      • Defining a class
        • Class members default to private, but the private keyword can also be used in order to be more explicit for larger or more complex classes (Unreal does this by default for all classes)
        • Typically function members are declared within the class but defined outside of it (declarations are usually in a header file, the definitions are in the .cpp file which includes that header files, this is the way Unreal does it)
          • When doing so, the scope resolution operator (::) needs to be used to define what the function definition should resolve to
      • Data members
        • Structs in C++ are essentially classes that have all of their members public by default and do not have member functions
        • Classes have their members private by default, and this is usually encouraged for data members - getter and setter function members (accessors) can operate on the private data, but the data itself should not be readily publicly accessible to the greater program to avoid unwanted/direct data manipulation
        • It's good practice to use a struct when you will only need data members and a class when you will need function members in addition to data members
      • Function members
        • Members of a class are accessed by dot notation, functions included, for example:
        •  
        • Const-qualified objects must be able to use const-qualified functions (class functions can have an overloaded version that is const-qualified to use for const objects if needed)
      • Constructors and destructors
        • Serve to create and destroy object data from a class
        • Called when the class is created or destroyed and can be overloaded
        • Implicit constructors
          • When no constructor is specifically declared, an implicit constructor is called when an object of a class is created
          • There are implicit constructors within C++ that exist for common scenarios (default, copy, etc)
        • Implicit destructors also exist and are called when the end of the code block is reached by the program
        • When defining constructors and destructors, the name of the class is used:
        • A ~ is used before the name of the class in order to specify a destructor
        • Default constructors can be made explicit rather than implicit in order to define default values for a class, for example - in the below constructor, all of the variables specified are being set to the global variable "unk" which is defined earlier in the program as a string with "unknown" as its value
        •  
        • Specifying constructors should take arguments, but can also use the : operator to assign those arguments to the data members of the created object of the class, like so:
        •  
        • Copy constructors take another object of the same class (or a different class if you want to overload it to take certain other data members and apply them to the new object) and pass the object as a reference in order to apply those data members (rhs stands for right-hand side, a common notation with copy constructors):
        •  
        • Assignment operators can be overloaded in order to implement copy constructor functionality
      • Explicit constructors
        • Constructors with only one parameter can be used for implicit type conversion
        • Default constructors can be put in the private section of a class to prevent them from being called (in order to protect data or force the user to implement the construction of a class in a certain way)
        • Compatible types can be passed to a constructor and will be implicitly converted to the appropriate type within the class as long as there is only one parameter in the constructor arguments (i.e. char to ASCII)
        • Constructors will do this type conversion automatically unless the explicit qualifier is added to the constructor definition which allows you to prevent the conversion if you don't want it to happen
      • Namespaces
        • You can define your own namespaces that can hold classes and members within them
        • Namespaces can use data types from other namespaces in order to define their own objects with the same name (i.e. std::string can be used to declare a namespace data type of namespace::string)
        • Commonly defined in header files along with the classes that use them
      • Self-referencing pointer
        • Member functions in C++ use the keyword this to provide a pointer to the current object
        • Commonly used to refer to members of a called object through other member functions on the same object
        • For example, an object with a function callMe() could be called by another function on the same object using this->callMe()
        • Helpful for things like operator overloading
      • Operator overloads
        • There are two ways to overload operators in C++
          • With member functions as part of a class definition
          • With separate non-member functions (see the next segment)
        • Class member functions can overload operators using the following syntax:
          • ClassName operator + (arguments);
          • Where the + would be the operator being overloaded
      • Non-member operators
        • Operator overloads can happen outside of the class definition as well in cases where the type is in flux and cannot be easily detected by the compiler from one side of the operator to another
        • Implicit constructors are used whenever the type does not match (i.e. it will convert an integer type into a class object due to there being an implicit constructor declared for an int input parameter)
        • Classes with implicit constructors for type conversion should be considered for using non-member overloads for as many operators as makes sense
      • Conversion operators
        • Conversion operators can also be overloaded by using the name of the data type to declare what the compiler should do when a conversion is requested (i.e. std string operations on your own class can be used, but an overloaded definition is required to tell the compiler what to do)
        • This allows you as the developer to fully control how your class is cast to other types in specific situations
      • Increment and decrement operators
        • When overloading increment and decrement operators, there are a few things to note:
          • Prefix overloads are passed as a reference with no arguments
          • Postfix overloads are passed normally with a single int argument (no matter actual type being incremented or decremented)
          • Postfix operations can't pass or return a reference as they need to make a copy first, they don't operate directly on the passed value - usually the prefix is preferred as they are a little less expensive unless you need to use a postfix
      • Allocating object memory
        • The new and delete operators can be used to allocate appropriate memory for classes
        • Any object allocated with new must also be deallocated with delete to avoid memory leaks
      • Functors 
        • The function operator can also be overloaded which can be used to create an object that works like a function, often called a functor
        • Helpful for situations where you need to keep an object's state or other context information
        • Allows the creation of similar classes easily which will operate in the same way when called
    • Class Inheritance
      • Overview of inheritance
        • Derived classes inherit the code from the base class and allows for re-use
        • There are three different levels of member access levels: public, protected, and private
          • Public
            • Available to all objects
          • Protected
            • Available to members of the same class (base and derived)
          • Private
            • Available to only the class that directly implements them

      • Simple inheritance
        • When declaring a derived class, the : operator is used to denote the left hand side is the parent/base class and the right hand side is the newly declared child/derived class, for example:
        •  
        • The access specifier before the name of the new derived class will almost always be public, but defines the way the class can be accessed
      • Accessing the base class
        • Functions defined in the base class will "exist" on the derived class, so nothing additional is needed in order to call a function from the base class on the derived class
        • However, the scope resolution operator can be used for greater clarity, for example you could call Animal::name() on the derived Dog class, or you could just call name() as long as the Dog class doesn't override that function
      • Friendship
        • Declaring a friend allows a derived class to access the private variables or constructors of the base class
        • The declaration is made inside the class' private section using the friend keyword followed by the class keyword and the class' name like so:
        • This exposes all private members of the base class to a derived class
        • This can also be used for specific functions outside the scope of any class, though it's not recommended as it undermines encapsulation - it's much more common to use accessors instead
      • Multiple inheritance
        • Derived classes can inherit from multiple base classes in C++
        • This allows for combination classes and the very extensive re-use of code, methods, and variables
        • It can create unnecessary complexity, so you should use it only when needed and re-evaluate the inheritance you're using to see if classes should be combined and/or isolated
      • Polymorphism
        • Member functions can be overloaded in derived classes
        • However, pointers to those classes may still look to use the base version
        • Marking a function with the virtual qualifier tells the compiler that the function may be overloaded, and to check in the derived classes to see - and use the overloaded version if so
          • Virtual destructors need to be declared in the base class if any virtual public functions are declared to allow for derived destructors to run if necessary
  • Object-Oriented Design LinkedIn Course - Jump to Top
    • Domain Modeling
      • Identify the objects
        • Create a conceptual model of the "things" in the application you need to be aware of, for example:
        • Find duplicates and remove from your list (i.e. "it" refers to the asteroid)
        • Remove behaviors from the list (i.e. offscreen)
        • Don't include the "system" in your list
      • Identify class relationships
        • Lay out all of your objects and connect the ones that need to interact with one another
        • Make a short note about how each interacts with the other
      • Object responsibilities
        • When identifying responsibilities, the primary guiding concept should be that any given object is responsible for itself
        • Avoiding giving too many responsibilities to the player or the system (or another object) is very important to avoid "god objects"
      • Chapter Quiz
    • Class Diagrams
      • When diagramming classes, you should display a class as what it is and what it can do (name, attributes, and behaviors) like so:
      • It's common to just define attribute names, but sometimes the data type and default values of an attribute are delineated
      • Behaviors/methods should have a name and a list of their input parameters
      • Visibility of members should be delineated with a + for public members and a - for private ones, for example:
      • Everything should be private whenever possible unless another object will need to use it - for manipulating data inside a class, use public getters and setters rather than publicizing the variables themselves (this helps to prevent unwanted modification)
      • Constructors
        • Overload constructors in order to define the creation of a class with certain defaults (or lack thereof)
      • Static variables
        • Hold across all instances of the class
      • Chapter Quiz
    • Inheritance and Composition
      • Identifying inheritance situations
        • Describes an "is a" relationship between super and subclasses
          • Sometimes, it's easier to phrase the relationship as an "is a type of" or an "is a kind of" relationship
          • For example, "A CargoShuttle is a type of Spaceship"
        • Classes should be inherited if they should share the same attributes/methods
      • Abstract and concrete classes
        • Abstract classes exist solely for the purpose of being inherited from
          • i.e. Spaceship exists but no "Spaceship" type object is ever spawned, only inherited classes such as CargoShuttle or WarpCruiser
          • Abstract classes have at least one abstract method (declared but not implemented), but don't need to have all their methods be abstract
        • Concrete classes
          • Meant to be instantiated, but not inherited from
          • Often thought of as the "final" inheritance in the line
      • Interfaces
        • A list of methods to be implemented by classes
        • Doesn't contain any behavior itself
          • Author's note: this is how C++ and blueprints interact at runtime
        • Interfaces are meant to represent a capability, whereas an abstract class is meant to represent a type, for example:
        • Good developers "program to an interface, not to an implementation" because it should be the developer's choice how to implement the methods defined in an interface for each situation
      • Aggregation
        • Collections of objects of a class type
        • Represented with a "has a" relationship (i.e. "A Fleet has a Spaceship"), which implicitly suggests any aggregate number of that object
        • When the aggregation is destroyed, the aggregated objects are not destroyed - they are just removed from the aggregation
      • Composition
        • Similar to aggregation, but with an "owns a" relationship (i.e. "A Spaceship owns an Engine")
        • When a composition is destroyed, the objects making it up are also destroyed
        • Construction and destruction of these composed objects should be taken care of by the composition object (i.e. the Spaceship constructor and destructor should take care of creating and destroying the Engine)
    • Course complete!
  • C++ for Python Programmers Runestone Course - Jump to Top
    • Chapter 6: Input and Output
      • File handling uses streams just like the in and out streams to the console
      • Libraries for file handling are held in <fstream>
      • File handle functions are similar to Python's in that they have an open mode (read, write, etc) and need to be closed when the program is finished with the file
      • In and out streams need to be closed separately
      • Reading from and writing to streams is handled by the >> and << operators, respectively
      • EOF (end-of-file) can be used in C++ by implementing the eof() method on a stream class in a while loop (returns a boolean)
        • Prevents writing into other files or the temporary buffer
      • All streams must be passed as a reference as they need to be changed by the stream buffer in order to be readable or writable
    • Chapter 7: Exception Handling
      • There are two main types of errors in programs - syntax errors (when the code is written incorrectly and fails to compile) and logic errors (the program executes but gives the wrong result)
      • In some cases, logic errors can lead to runtime errors where you attempt to do something like divide by 0 which will throw an exception and cause the program to terminate
      • Can use try-catch statements in order to "catch" an exception that has been raised, then provide an error to the user to let them know something went wrong
    • Runestone Course complete!
  • Unreal Converting Blueprint to C++ Course - Jump to Top
    • Lifecycle Methods
      • Unreal calls all of its constructors at edit time as well as when a level is played - although all constructors get called before any BeginPlay scripts get called (similar to a construction script versus an event graph in blueprints)
      • Just like with blueprints, each actor and component has its tick enabled by default, but ticking can be disabled for better performance if it is not needed - set the .cpp's bCanEverTick to false to disable tick
    • Actors v. Components
      • The choice between creating an actor or a component for any particular functionality usually comes down to what the two are able to do:
      •  
      • Technically actors can be composed within other actors, but in general you should use components for any composition inside of actors
      • Actors can do some of their own code, but for the most part it is suggested to use components for most of the code/functionality on an actor in order to increase the modularity available as a developer