CCC 8: Data - TEAM1771/Crash-Course GitHub Wiki
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 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 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.
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.
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, not1
. - 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
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:
- Access and Print Elements: We've used a range-based for loop (explained below) to access and print the original array elements.
- std::sort: We've sorted the array using the
std::sort
algorithm from the header. - size: We've used the
size()
method to get the size of the array. - front and back: We've used the
front()
andback()
methods to access the first and last elements of the array. - fill: We've used the
fill()
method to fill the array with zeros.
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
}
-
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. -
collection
This is the collection you want to iterate over, such as an array, std::vector, std::array, or any other iterable container. - 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.
For more about arrays, check this out!
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).
Here's a video if you want to learn more about vectors.
Casting is the process of converting data from one data type to another (like an integer to a double, or vise-versa).
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 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.
Learn more about casting from this video or this website for the geeks.