CCC 8: Data - TEAM1771/Crash-Course GitHub Wiki

Structs

We already briefly discussed structures in lesson 5: Objects, but I thought now would be a good time to bring back that idea and explore it a bit further.

Why and when do programmers use structs?

Probably the most frequent use-case is returning multiple variables. For example, let me show you a function we used in Robosaurus's code:

rejectState getRejectState()
{
    bool reject;
    bool reverse;

    // Logic
    ...

    return {reject, reverse};
}

This function analyzes the color of balls passing through our hopcore/elevator into the shooter.

If it notices the color of a ball is wrong, reject (we want to get rid of the ball) becomes true, and after the ball is shot out, if the next ball is the right color (we don't want to get rid of it), reverse becomes true for about 0.25 seconds. This asks us to reverse the hopcore for 0.25 seconds in order to allow enough time for the turret to aim back onto the target (it gains the turret 0.5 seconds in total, since the ball has to go back up the elevator).

Rather than having two functions trying to deal separately with rejection and reversal, it made much more sense to combine them, as their logic is intertwined. We want to return both as booleans, but we can't return 2 variables.

In comes structs!

  • Since each instance of a struct is only one variable but can contain multiple variables, we can use this to return both booleans.

In this case, rejectState is a struct defined as:

struct rejectState
{
    bool reject;
    bool reverse;
};

Now, what happens when we try to use the getRejectState() function? Sure, we could store the result as an instance of rejectState and then get the specific variables we need later:

rejectState const target_state = getRejectState();
if (target_state.reject)
    { ... }
if (target_state.reverse)
    { ... }

But, what if we could get rid of that intermediate variable, target_state?

Structured bindings

Structured bindings allow us to do just that.

  • We can define a variable name for each variable in the struct inside square brackets [] and move the type out to the front.
  • If you structure contains multiple different variable types, you can use auto instead of declaring the type, and the compiler will figure it out for you.

Check out this video for more.

bool const [need_to_reject, need_to_reverse] = targetRejectState();
if (need_to_reject)
    { ... }
if (need_to_reverse)
    { ... }

Both solutions (using and not using structure bindings) are valid, and there are situations where one makes more sense (especially if there are a lot of variables in that struct).

Enums

Enums are essentially just integers assigned a custom name - very similar to constexpr variables.

They are useful for giving seemingly meaningless numbers a much more understandable name.

enum PS5BUTTONS
{
    LEFT_BUMPER = 5,
    RIGHT_BUMPER = 6,
    LEFT_TRIGGER = 7,
    RIGHT_TRIGGER = 8,
    ...
};

They are also useful when a function might have multiple modes.

enum class GamePiece
{
    CONE, // Assigning a number is not required, this deafults to CONE = 0
    CUBE, // this defaults to CUBE = 1
    NEITHER // NEITHER = 2
};

void scoreGamePiece(GamePiece piece)
{
    if (piece == GamePiece::CONE)
        ...
    else if (piece == GamePiece::CUBE)
        ...
    else
        ...
}

enum class is the newer and recommended version of enum because it is safer to use (doesn't implicitely convert to int) Read more about that here.

Learn more about enums from this video.

Arrays

Now, what about storing a lot of related data/variables together?

Like, the temperature of each motor on the robot, or a list of commands the robot needs to execute? An array is very useful in these circumstances, as long as the number of variables you want to store is constant.

C-style array

The traditional method for using arrays in C is brackets, a style most languages adopted.

#include <iostream>

int main() {
    // Declare a C-style array of integers
    int temps[4]; // This creates space for 4 integers

    // Initialize the array elements
    temps[0] = 85;
    temps[1] = 50;
    temps[2] = 78;
    temps[3] = 70;

    // Access and print array elements
    std::cout << "Temps: ";
    for (int i = 0; i < 4; i++) {
        int temp = temps[i]; // Retrieves the data at index "i"
        std::cout << temp << " ";
        if (temp > 80)
            std::cout << "OVERHEATING!" << " ";
    }

    std::cout << std::endl;
}

Behind the scenes, an array is essentially just a group of variables stored in a row.

  • The number inside the brackets when accessing an array element simply tells the computer to look that many variables to the right of the first variable.

For example, the line temps[2] = 30 tells the computer to look two integers right of the beginning of the array and make that variable 30.

  • This means the index 0 is actually the beginning of an array, not 1.
  • This also means if you try to access an element outside the array, such as temps[10], you may end up messing up another variable in your program or causing very funky behavior!
  • An additional downside to C-style arrays is that there is no way to access the size of the array once it has been created

std::array

std::array is a container, or class, provided by the C++ Standard Library that works just like a C-style array, except with extra safety and more modern functionality.

We can use the same example as above to show how to declare and use a std::array.

#include <iostream>
#include <array>

int main() {
    // Declare a std::array of integers
    std::array<int, 4> temps; // This creates space for 4 integers

    // Initialize the array elements
    temps[0] = 85;
    temps[1] = 50;
    temps[2] = 78;
    temps[3] = 70;

    // Access and print array elements
    std::cout << "Temps: ";
    for (int i = 0; i < 4; i++) {
        int temp = temps[i]; // Retrieves the data at index "i"
        std::cout << temp << " ";
        if (temp > 80)
            std::cout << "OVERHEATING!" << " ";
    }

    std::cout << std::endl;
}
  • std::array attempts to maintain compatibility with existing coding styles, such as the use of brackets to access variables.

  • However, std::array helps protect against trying to access data outside the bounds of the array (i.e., trying to access element 4 (the fifth variable) in a 4-element array).

  • std::array also provides methods (functions inside a class) to more easily manage and access the data inside the array.

#include <iostream>
#include <array>
#include <algorithm> // For using std::sort

int main() {
    // Declare and initialize a std::array of integers
    std::array<int, 6> numbers = {7, 2, 9, 1, 5, 3};

    // Access and print the array elements
    std::cout << "Original numbers: ";
    for (const int& num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // Using std::sort to sort the array in ascending order
    std::sort(numbers.begin(), numbers.end());

    // Access and print the sorted array elements
    std::cout << "Sorted numbers: ";
    for (const int& num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // Using std::array's size method
    std::cout << "Array size: " << numbers.size() << std::endl;

    // Using std::array's front and back methods
    std::cout << "First element: " << numbers.front() << std::endl;
    std::cout << "Last element: " << numbers.back() << std::endl;

    // Using std::array's fill method
    numbers.fill(0); // Fill the array with zeros

    // Access and print the updated array elements
    std::cout << "Updated numbers: ";
    for (const int& num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

In this example, we've used several methods provided by the std::array class:

  1. Access and Print Elements: We've used a range-based for loop (explained below) to access and print the original array elements.
  2. std::sort: We've sorted the array using the std::sort algorithm from the header.
  3. size: We've used the size() method to get the size of the array.
  4. front and back: We've used the front() and back() methods to access the first and last elements of the array.
  5. fill: We've used the fill() method to fill the array with zeros.

Range-based for loop

As shown in the example above, a range-based for loop is a convenient feature in C++ that simplifies iterating (going through) through elements of a collection, such as a std::array (but other types work as well). It's designed to make your code more concise and easier to read when you need to perform actions on each element of a collection.

Let's break down the parts.

for (element_type element : collection) 
{
    // Code to be executed for each element
}
  1. element_type: This is the variable type of each individual element in the collection. Each loop (or iteration) of the for loop will declare a variable of this type inside the loop to hold the current element as you go through each element in the collection.
  2. collection This is the collection you want to iterate over, such as an array, std::vector, std::array, or any other iterable container.
  3. The code inside the loop's block will be executed for each element in the collection. The variable you declared will hold the current element's value during each iteration.

Example:

#include <iostream>
#include <array>

int main() 
{
    std::array<int> numbers = {1, 2, 3, 4, 5};

    // Using a range-based for loop to iterate through the array
    for (int num : numbers) 
    {
        std::cout << num << " "; // Print each number followed by a space
    }
}

You can also use references if you wish to avoid copying each variable, or if you wish to modify the variables. You can also use const if desired.

#include <iostream>
#include <array>

int main() 
{
    std::array<int> numbers = {1, 2, 3, 4, 5};

    for (int &num : numbers) // Uses references
    {
        num++;
    }

    // numbers is now {2, 3, 4, 5, 6}

    for (int const &num : numbers) // Avoids copying each variable, useful if variable is large
    {
        std::cout << num << " ";
    }
}

However, avoiding copying is basically pointless for integers, as these variables are small.

Extra Resources

For more about arrays, check this out!

Vector

The one main restriction with arrays is that the size is restricted. There are many cases in which the amount of data we want to store changes. For example, if we wanted to keep track of every error message the robot issued, we would want to be able to grow/shrink the size of the container in response to how many error messages are issued.

Creating and initalizing a vector is very similar to std::array

#include <vector>

std::vector<int> my_vector; // Create an empty integer vector
std::vector<string> names = {"Alice", "Bob", "Charlie"}; // Initialize a vector with values

Adding, accessing, and removing elements works as follows:

#include <vector>
#include <string>

std::vector<std::string> errors;
errors.push_back("Motor 2 not detected");
errors.push_back("Joystick unplugged");
errors.push_back("Battery voltage low");

std::string first_error = errors[0]; // Access the first element ("Motor 2 not detected")
std::string second_error = errors.at(1); // Another way of accessing, this time the second element ("Joystick unplugged")
auto last_error = errors.back(); // Returns a reference to the last error ("Battery voltage low")

int size = errors.size(); // Size is 3

numbers.pop_back(); // Removes the last error message

int size = errors.size(); // Size is now 2

Vectors can be used essentaily the exact same as std::array and other containers in range-based for loops (explained above).

Because of the changing size of vectors, it is especially important to be careful and avoid attempting to access elements outside the size of the array (can crash the code).

Extra Resources

Here's a video if you want to learn more about vectors.

Casting

Casting is the process of converting data from one data type to another (like an integer to a double, or vise-versa).

C-style casts (avoid)

C-Style casting, also known as "old-style casting," uses parentheses and the desired data type in front of the value to perform the conversion. For example:

double pi = 3.14159;
int integer_pi = (int)pi; // C-style casting

integer_pi is equal to 3

Risk of Data Loss: C-style casting can lead to data loss, especially when converting between floating-point and integer types. Be cautious when using it.

Static casts (recommended)

Static casting is a safer and more readable alternative to C-style casting. It uses the static_cast keyword to convert between types. For example:

double pi = 3.14159;
int integer_pi = static_cast<int>(pi); // Static casting

integer_pi is equal to 3

Advantages of Static Casting

  • Type Safety: Static casting provides compile-time checks to catch potential errors, reducing the risk of runtime issues.
  • Clarity: It makes the code more readable by explicitly indicating the type conversion.

Extra Resources

Learn more about casting from this video or this website for the geeks.

⚠️ **GitHub.com Fallback** ⚠️