C++ for Robotics: From Fundamentals to Application
Syllabus
Course Description
Welcome, future roboticist! This 16-week course is your launchpad into the exciting world where code meets the physical world. C++ is the powerhouse language of the robotics industry, prized for its performance, control over hardware, and vast ecosystem. Whether you dream of building autonomous drones, nimble robot arms, or rovers that explore other planets, a strong foundation in C++ is non-negotiable.
This course is designed to take you from the absolute basics of C++ programming to applying its advanced concepts in a robotics context. We’ll start with fundamental building blocks like variables and loops, but we’ll always frame them with robotics in mind—think motor speeds and sensor data, not just abstract numbers. We will then dive into Object-Oriented Programming (OOP) to model complex robots and their behaviors, explore the Standard Template Library (STL) for powerful data handling, and even touch on advanced topics like multithreading and the concepts behind the Robot Operating System (ROS). By the end, you won’t just know C++; you’ll know how to think in C++ for robotics.
Primary Learning Objectives
Upon successful completion of this course, you will be able to:
- Master C++ Fundamentals: Write, compile, and debug C++ programs using variables, control structures, loops, and functions.
- Model Robotic Systems: Utilize Object-Oriented Programming (OOP) principles like classes, inheritance, and polymorphism to create modular and scalable code that represents real-world robots.
- Manage Data Efficiently: Employ data structures like arrays, vectors, and maps from the Standard Template Library (STL) to handle sensor data, waypoints, and state information.
- Understand Memory Control: Grasp the concepts of pointers and memory management, a critical skill for performance-intensive robotics applications.
- Apply C++ to Robotics Problems: Design and implement solutions for common robotics tasks, such as data logging, state management, and basic control logic.
- Build a Complete Project: Develop a simulated autonomous navigation system from the ground up, integrating multiple concepts learned throughout the course.
Prerequisites
- A motivated and curious mind!
- Basic computer literacy.
- Prior experience with any programming language (like Python or JavaScript) is helpful but not strictly required. We will start from the basics.
Necessary Materials
- Software:
- A modern C++ compiler (like GCC/G++ or Clang). This is often pre-installed on Linux/macOS. For Windows, you can install it via MSYS2 or Windows Subsystem for Linux (WSL).
- A code editor or IDE. Visual Studio Code with the C/C++ extension pack is a fantastic, free option. Other choices include CLion (paid) or a simple text editor like Sublime Text.
- CMake: A build system generator that is standard in the C++ world, especially in robotics.
- Hardware (Optional but Recommended for later lessons):
- An Arduino Uno or a Raspberry Pi. These affordable microcontrollers and single-board computers are excellent platforms for applying C++ to physical hardware.
Course Structure
- Weeks 1-7: Part 1: The C++ Core
- Week 8: Mid-Course Review & Catch-up
- Weeks 9-14: Part 2: Advanced C++ for Robotics
- Weeks 15-16: Part 3: Final Project
Lesson 1: Introduction to C++ & Your First Program (Hello, Robot!)
Learning Objectives
- Understand why C++ is a dominant language in robotics.
- Set up a complete C++ development environment (compiler, editor).
- Write, compile, and run your very first C++ program.
Key Vocabulary
- Compiler: A special program that translates your human-readable C++ code into machine code (binary 0s and 1s) that a computer’s processor can execute.
- IDE (Integrated Development Environment): A software application that provides comprehensive facilities to programmers for software development. It normally consists of a source code editor, build automation tools, and a debugger.
#include
: A preprocessor directive that tells the compiler to include the contents of a specified file (usually a header file) in your program.main()
function: The mandatory starting point for execution in every C++ program.std::cout
: The standard character output stream. It’s the primary way to print text to your console.
Lesson Content
Hello and welcome! So, why C++ for robotics? Three big reasons: speed, control, and ecosystem.
- Speed: Robots need to think fast. They process massive amounts of sensor data and must react in real-time. C++ is a compiled language, which means your code is translated directly into fast, native machine code. Languages like Python are interpreted, which adds a layer of translation at runtime, making them inherently slower.
- Control: Robotics is all about interacting with hardware—motors, sensors, and actuators. C++ gives you low-level memory management capabilities, allowing you to control precisely how your program uses resources, which is critical for performance and predictability.
- Ecosystem: The most influential robotics framework, the Robot Operating System (ROS), is written primarily in C++. Major libraries for computer vision (OpenCV), point cloud processing (PCL), and physics simulation (Gazebo) all have robust C++ APIs.
Setting Up Your Environment
Your first task is to set up your workshop. You need two tools: a compiler and a code editor.
- Compiler: G++ is the most common. On Linux, you can install it with
sudo apt-get install build-essential
. On Mac, installing Xcode Command Line Tools will do the trick. On Windows, we recommend setting up WSL or MSYS2. - Editor: We highly recommend Visual Studio Code (VS Code). It’s free, powerful, and has excellent C++ support.
Your First Program: “Hello, Robot!”
Let’s write a program. It’s a tradition to start with “Hello, World!”, but we’ll give it a robotics twist. Create a new file named hello_robot.cpp
.
// This is a comment. The compiler ignores it.
// It's for humans to read!
// The iostream library lets us handle input and output (like printing to the screen).
#include <iostream>
// The main function is where our program starts.
int main() {
// std::cout is used to "output" text to the console.
// << is the stream insertion operator.
// std::endl adds a new line.
std::cout << "Hello, Robot! System online." << std::endl;
// A return value of 0 tells the operating system that the program ran successfully.
return 0;
}
How to Compile and Run
- Open a terminal or command prompt.
- Navigate to the directory where you saved
hello_robot.cpp
. - Compile the program by typing:
g++ hello_robot.cpp -o hello_robot
g++
is the command to call the compiler.hello_robot.cpp
is your source file.-o hello_robot
tells the compiler to name the output executable filehello_robot
.
- Run the program by typing:
./hello_robot
(on Linux/Mac) orhello_robot.exe
(on Windows).
You should see Hello, Robot! System online.
printed to your console. Congratulations, you’re officially a C++ programmer!
Hands-On Example
Modify your hello_robot.cpp
program to introduce your robot. It should print three separate lines:
- “Robot Name: Unit 734”
- “Status: Nominal”
- “Mission: Begin Exploration”
Hint: You can use std::cout
multiple times, or you can chain outputs together with <<
.
Lesson 2: Variables, Data Types, and Sensor Readings
Learning Objectives
- Understand and use fundamental C++ data types (
int
,double
,char
,bool
). - Declare, initialize, and modify variables to store data.
- Use variables to represent common robotics data like sensor values and motor speeds.
Key Vocabulary
- Variable: A named storage location in memory that holds a value.
- Data Type: A classification that specifies which type of value a variable can hold and what kind of mathematical, relational, or logical operations can be applied to it without causing an error.
- Declaration: The process of defining a variable’s name and data type.
- Initialization: The process of assigning an initial value to a variable at the time of its declaration.
const
: A keyword that makes a variable’s value unchangeable after initialization.
Lesson Content
A robot is useless without data. It needs to know its battery level, the distance to an obstacle, the angle of its arm, or whether a gripper is open or closed. Variables are how we store this data in our program.
Think of a variable as a labeled box. The label is the variable’s name, and the contents are its value. The type of box determines what you can put inside.
Core Data Types in C++
C++ has several built-in data types. Here are the most essential ones for robotics:
Data Type | Description | Robotics Example |
---|---|---|
int | Integers (whole numbers) | int motor_speed = 255; |
double | Double-precision floating-point numbers (decimals) | double distance_cm = 34.7; |
char | A single character | char mode = 'A'; // 'A' for Autonomous |
bool | Boolean (can be true or false ) | bool obstacle_detected = false; |
std::string | A sequence of characters | std::string robot_name = "RoverX"; |
Declaring and Using Variables
Let’s write a program to simulate reading from a distance sensor.
#include <iostream>
#include <string> // Need to include this for std::string
int main() {
// Declaration and Initialization
std::string sensor_name = "Ultrasonic Ranger";
int sensor_pin = 7;
double distance_cm = 42.5; // Our first simulated reading
bool is_active = true;
// Let's print a status report
std::cout << "--- Sensor Status ---" << std::endl;
std::cout << "Name: " << sensor_name << std::endl;
std::cout << "Pin: " << sensor_pin << std::endl;
std::cout << "Active: " << is_active << std::endl; // bools often print as 1 (true) or 0 (false)
std::cout << "Initial Reading: " << distance_cm << " cm" << std::endl;
// Now let's update the reading
std::cout << "\nTaking new reading..." << std::endl;
distance_cm = 35.2; // We re-assign a new value to the variable
std::cout << "New Reading: " << distance_cm << " cm" << std::endl;
// Using constants for things that don't change
const double SPEED_OF_SOUND_CM_PER_S = 34300.0;
std::cout << "Reference Speed of Sound: " << SPEED_OF_SOUND_CM_PER_S << std::endl;
// SPEED_OF_SOUND_CM_PER_S = 1.0; // This line would cause a compiler error!
return 0;
}
In this code, distance_cm
is a variable because a sensor’s reading changes. SPEED_OF_SOUND_CM_PER_S
is a const
because the physical constant doesn’t change during our program’s execution. Using const
helps prevent accidental bugs and makes your code’s intent clearer.
Hands-On Example
Write a C++ program that models a robot’s battery status.
- Create a
double
variable to store the battery percentage (e.g.,95.5
). - Create a
bool
variable to indicate if the robot is currently charging. - Create an
int
variable for the estimated minutes of runtime remaining. - Print a full status report to the console using these variables. The output should be clearly formatted and easy to read.
Lesson 3: Control Flow: Making Decisions with if
, else
, and switch
Learning Objectives
- Understand and use
if
,else if
, andelse
statements to control program execution based on conditions. - Utilize comparison (
==
,!=
,<
,>
) and logical (&&
,||
,!
) operators to build complex conditions. - Implement a
switch
statement for multi-way branching based on a single variable.
Key Vocabulary
- Control Flow: The order in which individual statements, instructions, or function calls of a program are executed or evaluated.
- Condition: An expression that evaluates to a Boolean value (
true
orfalse
). if
statement: Executes a block of code if its condition is true.else
statement: Executes a block of code if the precedingif
condition is false.- Logical Operators: Operators used to combine or modify logical conditions.
&&
(AND),||
(OR),!
(NOT).
Lesson Content
A robot that can’t make decisions is just a machine following a fixed script. True intelligence (even a little!) comes from reacting to the environment. Control flow statements allow our robot to do just that.
The if-else
Statement
The most fundamental decision-making tool is the if
statement. It’s like a fork in the road for your code.
Imagine a robot with a forward-facing distance sensor. Its most basic logic is: “If an obstacle is too close, stop. Otherwise, keep moving forward.”
Let’s code that.
#include <iostream>
int main() {
double distance_to_obstacle_cm = 25.0; // Simulated sensor reading
const double MIN_SAFE_DISTANCE_CM = 30.0; // Our safety threshold
std::cout << "Sensor reads: " << distance_to_obstacle_cm << " cm." << std::endl;
std::cout << "Safe distance is: " << MIN_SAFE_DISTANCE_CM << " cm." << std::endl;
if (distance_to_obstacle_cm < MIN_SAFE_DISTANCE_CM) {
// This block runs ONLY if the condition is true
std::cout << "ACTION: Obstacle detected! Halting motors." << std::endl;
} else {
// This block runs ONLY if the condition is false
std::cout << "ACTION: Path is clear. Proceeding forward." << std::endl;
}
return 0;
}
Run this code. Then, try changing distance_to_obstacle_cm
to 45.0
and run it again to see the different outcomes.
Adding More Choices with else if
What if we want more nuanced behavior? “If the obstacle is very close, reverse. If it’s just a little close, slow down. Otherwise, full speed ahead.”
#include <iostream>
int main() {
double distance_cm = 45.0;
const double EMERGENCY_STOP_DIST = 20.0;
const double SLOW_DOWN_DIST = 50.0;
if (distance_cm < EMERGENCY_STOP_DIST) {
std::cout << "DANGER! Reversing motors!" << std::endl;
} else if (distance_cm < SLOW_DOWN_DIST) {
std::cout << "Caution. Reducing speed." << std::endl;
} else {
std::cout << "Path clear. Full speed ahead." << std::endl;
}
return 0;
}
Combining Conditions with Logical Operators
Let’s say we have two sensors: a front sensor and a battery sensor. We only want to move if the path is clear AND the battery is not low.
#include <iostream>
int main() {
bool front_path_clear = true;
double battery_percentage = 15.0;
// We use && for AND. Both sides must be true.
if (front_path_clear == true && battery_percentage > 20.0) {
std::cout << "Proceeding." << std::endl;
} else {
std::cout << "Cannot proceed. Checking diagnostics..." << std::endl;
if (front_path_clear == false) {
std::cout << "- Obstacle detected." << std::endl;
}
if (battery_percentage <= 20.0) {
std::cout << "- Battery is low." << std::endl;
}
}
return 0;
}
Hands-On Example
Write a program that simulates a robot arm’s gripper control.
- Create a
char
variable namedcommand
and initialize it to'g'
. - Create a
bool
variable namedis_gripper_open
and initialize it totrue
. - Write an
if-else if-else
structure that checks thecommand
variable:- If
command
is'g'
(grab) AND the gripper is open, print “Closing gripper.” and setis_gripper_open
tofalse
. - If
command
is'r'
(release) AND the gripper is not open, print “Opening gripper.” and setis_gripper_open
totrue
. - For any other command, print “Invalid command.”
- If
- Test your code with different initial values for
command
andis_gripper_open
to see all paths execute correctly.
Lesson 4: Loops: for
and while
for Repetitive Tasks
Learning Objectives
- Use
while
loops to repeat a block of code as long as a condition is true. - Use
for
loops to repeat a block of code a specific number of times. - Understand the concepts of loop control (initialization, condition, update) and infinite loops.
Key Vocabulary
- Loop: A control flow statement that allows code to be executed repeatedly.
while
loop: A loop that executes as long as its condition evaluates totrue
. The condition is checked before each iteration.for
loop: A loop that combines initialization, condition checking, and an update expression into a single line, typically used for iterating a known number of times.- Iteration: A single execution of the code block within a loop.
- Infinite Loop: A loop whose condition never becomes false, causing it to run forever (or until the program is terminated).
Lesson Content
Robots are masters of repetition. They check their sensors hundreds of times per second, precisely rotate a motor by a specific number of steps, or process every pixel in an image. Loops are the programming construct that makes this possible.
The while
Loop: Repeating Until a Condition Changes
A while
loop is the simplest kind of loop. It says: “While this condition is true, keep doing this.” It’s perfect for situations where you don’t know in advance how many times you need to repeat.
Consider a robot trying to charge its battery. It should stay plugged in while its battery is below 100%.
#include <iostream>
int main() {
double battery_level = 85.0;
const double FULL_CHARGE = 100.0;
int charge_cycles = 0;
std::cout << "Starting charge cycle. Current level: " << battery_level << "%" << std::endl;
while (battery_level < FULL_CHARGE) {
// This code block will repeat
battery_level += 1.0; // Simulate charging
charge_cycles++;
std::cout << "Charging... Level: " << battery_level << "%, Cycle: " << charge_cycles << std::endl;
}
std::cout << "Charge complete! Detaching from charger." << std::endl;
return 0;
}
The condition battery_level < FULL_CHARGE
is checked before every single iteration. As soon as battery_level
reaches 100.0
, the condition becomes false, and the loop terminates.
The for
Loop: Repeating a Known Number of Times
A for
loop is ideal when you know exactly how many times you want to loop. It neatly packages the three parts of a loop: initialization, condition, and update.
Imagine a robot arm needs to tighten a screw by turning it 5 times.
#include <iostream>
int main() {
// The for loop has three parts in the parentheses, separated by semicolons:
// 1. Initialization: `int i = 0;` (runs once before the loop starts)
// 2. Condition: `i < 5;` (checked before each iteration)
// 3. Update: `i++` (runs at the end of each iteration)
for (int i = 0; i < 5; i++) {
std::cout << "Turning screw, rotation " << i + 1 << " of 5." << std::endl;
}
std::cout << "Screw is tight." << std::endl;
return 0;
}
This is much cleaner than writing the equivalent while
loop:
// Equivalent using a while loop
int i = 0;
while (i < 5) {
std::cout << "Turning screw, rotation " << i + 1 << " of 5." << std::endl;
i++;
}
Hands-On Example
You are writing a startup sequence for a robot’s LIDAR scanner (a laser-based distance sensor). The scanner needs to spin up and perform a calibration sequence that involves taking 10 readings.
- Use a
for
loop that runs exactly 10 times. - Inside the loop, print a message like “Taking LIDAR reading #1…”, “Taking LIDAR reading #2…”, etc.
- After the loop finishes, print “LIDAR calibration complete. System is ready.”
Lesson 5: Functions: Creating Reusable Code Blocks for Robot Actions
Learning Objectives
- Understand the purpose of functions for organizing and reusing code.
- Write functions that take parameters (inputs) and have a return type (output).
- Decompose a complex robotics task into smaller, manageable functions.
Key Vocabulary
- Function: A named block of code that performs a specific task and can be called (or invoked) from other parts of the program.
- Parameter: A variable listed in a function’s declaration that is used to pass information into the function.
- Return Value: The value that a function sends back to the code that called it. The
void
keyword is used for functions that do not return a value. - Function Signature: The function’s name, its parameters, and its return type. For example,
double calculateDistance(int echo_time)
. - Function Call: The act of executing a function.
Lesson Content
As our robot programs get more complex, putting all the code inside the main()
function becomes a messy nightmare. Imagine trying to read the code for a humanoid robot if everything—walking, grasping, seeing, talking—was all jumbled together!
Functions are the solution. They are like mini-programs that you can name and call whenever you need them. This makes your code modular, reusable, and much easier to read and debug.
Anatomy of a Function
Let’s define a simple function that simulates a robot’s emergency stop procedure.
#include <iostream>
// --- Function Definition ---
// return_type function_name(parameter_type parameter_name, ...)
// 'void' means this function does not return any value.
void performEmergencyStop() {
std::cout << "!!! EMERGENCY STOP !!!" << std::endl;
std::cout << "Cutting power to all motors." << std::endl;
std::cout << "Engaging brakes." << std::endl;
}
int main() {
// This is the function call.
performEmergencyStop();
return 0;
}
Functions with Parameters and Return Values
Now let’s make a more useful function. This one will take a battery percentage as input (a parameter) and return a bool
indicating if the level is critically low.
#include <iostream>
// This function takes one 'double' parameter and returns a 'bool'.
bool isBatteryLow(double percentage) {
const double CRITICAL_LEVEL = 20.0;
if (percentage < CRITICAL_LEVEL) {
return true;
} else {
return false;
}
}
// A function to control wheel motors
void setMotorSpeeds(int left_speed, int right_speed) {
std::cout << "Setting motors: Left=" << left_speed << ", Right=" << right_speed << std::endl;
}
int main() {
double current_battery = 17.5;
// Call isBatteryLow and store its return value in a variable.
bool battery_is_critical = isBatteryLow(current_battery);
if (battery_is_critical) {
std::cout << "Battery is critical. Stopping robot." << std::endl;
setMotorSpeeds(0, 0); // Call another function
} else {
std::cout << "Battery is OK. Proceeding with mission." << std::endl;
setMotorSpeeds(200, 200);
}
return 0;
}
Notice how main()
is now much cleaner. It describes the high-level logic (“check the battery, then set speeds”), while the details of how to do those things are hidden away in their respective functions. This is a core principle of good software design called abstraction.
Hands-On Example
Create a program for a simple robot arm.
- Write a function called
calculateJointAngle
.- It should accept two
double
parameters:desired_position_mm
andarm_segment_length_mm
. - It should calculate the required angle (in degrees) using a simplified formula:
angle = (desired_position_mm / arm_segment_length_mm) * 90.0
. - It should return the calculated angle as a
double
.
- It should accept two
- In your
main()
function, callcalculateJointAngle
with some sample values (e.g., position 50mm, arm length 100mm). - Store the returned angle in a variable and print a message like: “To reach 50.0mm, the joint angle must be 45.0 degrees.”
Lesson 6: Arrays, Vectors, and Storing Sensor Data Streams
Learning Objectives
- Store and access sequences of data using C-style arrays.
- Use
std::vector
, a dynamic and safer alternative to C-style arrays. - Iterate over the elements of an array or vector using a loop.
Key Vocabulary
- Array: A fixed-size collection of elements of the same data type, stored contiguously in memory.
std::vector
: A sequence container from the C++ Standard Library that can change its size dynamically.- Element: An individual item within an array or vector.
- Index: A number that identifies the position of an element in an array or vector. In C++, indexing starts at 0.
- Dynamic Size: The ability of a container like
std::vector
to grow or shrink as needed during program execution.
Lesson Content
A single sensor reading is a snapshot in time. To understand the world, a robot needs a history—a sequence of readings. To follow a path, it needs a list of waypoints. Arrays and vectors are the tools we use to store these ordered collections of data.
C-Style Arrays: The Old Way
The most basic way to store a list is a C-style array. You must declare its size upfront, and it cannot be changed later.
#include <iostream>
int main() {
// Declare an array of 5 integers. The size MUST be known at compile time.
int sonar_pings[5];
// Assign values to each element using its index (0 to 4)
sonar_pings[0] = 300;
sonar_pings[1] = 250;
sonar_pings[2] = 260;
sonar_pings[3] = 400;
sonar_pings[4] = 380;
// Access and print an element
std::cout << "The third ping was: " << sonar_pings[2] << std::endl; // Index 2 is the 3rd element
// Use a for loop to process the entire array
double total = 0.0;
for (int i = 0; i < 5; i++) {
total += sonar_pings[i];
}
std::cout << "Average ping distance: " << total / 5.0 << std::endl;
return 0;
}
The Problem with Arrays: They are rigid. What if you need to store 6 pings? Or only 2? And what happens if you accidentally try to access sonar_pings[5]
? This is called an out-of-bounds access, a common and dangerous bug.
std::vector
: The Modern, Safer Way
Enter std::vector
. It’s part of the C++ Standard Template Library (STL) and behaves like a “smart array.” It manages its own memory and can grow or shrink on demand. You should almost always prefer std::vector
over C-style arrays.
#include <iostream>
#include <vector> // Don't forget to include the vector header!
#include <numeric> // For std::accumulate
int main() {
// Create a vector of doubles. No initial size needed.
std::vector<double> lidar_readings;
// Add elements to the back of the vector. It grows automatically!
lidar_readings.push_back(1.2);
lidar_readings.push_back(1.3);
lidar_readings.push_back(1.1);
lidar_readings.push_back(2.5); // Obstacle!
std::cout << "Number of readings collected: " << lidar_readings.size() << std::endl;
std::cout << "The first reading was: " << lidar_readings[0] << std::endl;
// Iterating with a range-based for loop (the easiest way!)
std::cout << "All readings:" << std::endl;
for (double reading : lidar_readings) {
std::cout << "- " << reading << " meters" << std::endl;
}
return 0;
}
std::vector
provides many useful features, like .size()
to get the current number of elements, .push_back()
to add an element, .pop_back()
to remove the last element, and .clear()
to empty it. It’s an incredibly powerful and essential tool.
Hands-On Example
You are programming a path for a robot to follow. The path is defined by a sequence of X-Y coordinates.
- Create two
std::vector<double>
: one namedpath_x
and one namedpath_y
. - Use
push_back()
to add the following sequence of waypoints to your vectors:(1.0, 2.5)
,(3.0, 4.0)
,(5.5, 3.0)
. - Write a
for
loop that iterates fromi = 0
up to the size of the vectors. - Inside the loop, print each waypoint in a format like:
"Waypoint 1: (X=1.0, Y=2.5)"
.
Lesson 7: Pointers & Memory Management: The Power and Responsibility
Learning Objectives
- Understand what a memory address is and what a pointer stores.
- Declare pointers, use the address-of (
&
) and dereference (*
) operators. - Recognize why pointers are crucial for performance and hardware interaction in robotics.
Key Vocabulary
- Memory Address: A unique identifier for a location in your computer’s RAM, usually represented as a hexadecimal number.
- Pointer: A special type of variable that stores a memory address as its value.
- Address-of Operator (
&
): An operator that, when placed before a variable name, returns that variable’s memory address. - Dereference Operator (
*
): An operator that, when placed before a pointer variable, accesses the value stored at the memory address the pointer is holding. nullptr
: A keyword representing a null pointer, i.e., a pointer that doesn’t point to any valid memory address.
Lesson Content
This is a challenging but vital topic. Pointers are one of the things that give C++ its power and speed. In robotics, you often need to interact directly with hardware registers or work with large amounts of data (like images or point clouds) without making slow, memory-intensive copies. Pointers are the key.
What is a Pointer?
Imagine your computer’s memory (RAM) is a giant street of houses. Each house has a unique address (e.g., “123 Main St”). Every variable you declare lives in one of these houses.
A normal variable stores data directly: int house_number = 123;
A pointer doesn’t store the data itself. It stores the address of the house where the data lives. It “points to” the data.
#include <iostream>
int main() {
int motor_speed = 255; // A regular variable
// Declare a pointer. The '*' indicates it's a pointer.
// 'int*' means "a pointer that points to an integer".
int* motor_speed_ptr = nullptr;
// Use the address-of operator '&' to get the memory address of motor_speed.
// Now, motor_speed_ptr holds the address where 255 is stored.
motor_speed_ptr = &motor_speed;
std::cout << "Value of motor_speed: " << motor_speed << std::endl;
std::cout << "Address of motor_speed: " << &motor_speed << std::endl;
std::cout << "Value of motor_speed_ptr (it's an address): " << motor_speed_ptr << std::endl;
// To get the value AT the address the pointer holds, we DEREFERENCE it with '*'.
std::cout << "Value pointed to by motor_speed_ptr: " << *motor_speed_ptr << std::endl;
// You can also change the original variable's value THROUGH the pointer.
std::cout << "\nChanging value via pointer..." << std::endl;
*motor_speed_ptr = 150; // Go to the address and change the value there to 150.
std::cout << "New value of motor_speed: " << motor_speed << std::endl;
return 0;
}
Why is this useful in robotics?
- Efficiency: Imagine you have a function that needs to process a giant 10-megabyte camera image. If you pass the image to the function normally, the computer copies all 10 megabytes, which is slow. If you pass a pointer to the image, you’re only passing a tiny 8-byte memory address. The function can then use that address to work with the original image data directly.
- Hardware Interaction: In low-level programming (like on an Arduino), a specific hardware control register (e.g., the one that controls a motor’s PWM signal) exists at a fixed memory address. You can create a pointer to that exact address to read from or write to the hardware directly.
Pointers are a double-edged sword. With this power comes responsibility. Using a pointer that is null (nullptr
) or points to memory you don’t own will crash your program. This is a rite of passage for every C++ programmer!
Hands-On Example
Write a program that demonstrates passing a variable to a function “by reference” using a pointer.
- In
main()
, create adouble
variable namedrobot_x_position
and initialize it to10.0
. - Write a function called
moveRobot
that takes a pointer to a double (double*
) as its parameter, representing the robot’s position. - Inside
moveRobot
, dereference the pointer and add5.0
to the value it points to. Print a message from inside the function, like “Robot moved. New position: [new value]”. - In
main()
, print therobot_x_position
before callingmoveRobot
. - Call
moveRobot
, passing it the address ofrobot_x_position
(using&
). - In
main()
again, printrobot_x_position
after the function call to confirm that its value was permanently changed by the function.
Week 8: Mid-Course Review & Catch-up
This week is dedicated to reinforcing the foundational concepts from Lessons 1-7.
- Review: Go back through each lesson. Re-read the content and re-do the hands-on examples.
- Consolidate: Can you combine concepts? Try writing a single program that uses variables, loops, functions, and a
std::vector
to simulate a robot patrolling a series of waypoints and making decisions based on a simulated sensor. - Experiment: What happens if you try to access an array out of bounds? What happens if you dereference a
nullptr
? Safely causing these errors and seeing the result is an excellent way to learn. - Look Ahead: Get ready for Part 2, where we will start building more complex and realistic robot models using Object-Oriented Programming!
Lesson 8: Structs & Classes: Modeling a Robot in Code
Learning Objectives
- Group related variables into a single unit using
struct
. - Define a
class
, understanding the difference betweenpublic
andprivate
members. - Create objects (instances) of a class and access their members.
Key Vocabulary
struct
(Structure): A composite data type that groups variables under a single name. By default, its members arepublic
.class
: A blueprint for creating objects. It combines data (member variables) and methods (member functions) that operate on that data. By default, its members areprivate
.- Object (or Instance): A concrete entity created from a class. If
Robot
is the class (the blueprint), thenmy_rover
could be an object. - Member: A variable or function that is part of a class or struct.
public
: A keyword making a class member accessible from outside the class.private
: A keyword making a class member accessible only from within the class itself.
Lesson Content
So far, our robot’s data has been scattered across separate variables (motor_speed
, battery_level
, etc.). This gets messy. What if we have two robots? We’d need robot1_motor_speed
, robot2_motor_speed
… yuck.
There’s a better way! We can create our own custom data types that group all the related data and behaviors together. This is the first step into Object-Oriented Programming (OOP).
struct
: A Simple Data Bundle
A struct
is the simplest way to group data. Let’s create a type to hold a 2D waypoint.
#include <iostream>
// Define a new type called Waypoint
struct Waypoint {
double x;
double y;
std::string name;
};
int main() {
// Create a variable (an "instance") of our new type
Waypoint start_point;
// Access the members using the dot (.) operator
start_point.x = 0.0;
start_point.y = 0.0;
start_point.name = "Home Base";
Waypoint destination;
destination.x = 15.5;
destination.y = -7.2;
destination.name = "Objective";
std::cout << "Heading from " << start_point.name
<< " (" << start_point.x << ", " << start_point.y << ")"
<< " to " << destination.name
<< " (" << destination.x << ", " << destination.y << ")"
<< std::endl;
return 0;
}
class
: Data + Behavior
A class
takes this a step further. It bundles not just data (member variables), but also the functions that operate on that data (member functions or “methods”). This is the core idea of OOP.
Let’s model a simple robot. A robot has data (battery_percentage
, position
) and it has behaviors (move
, recharge
).
#include <iostream>
// Define the "blueprint" for a SimpleRobot
class SimpleRobot {
public: // Things accessible from outside the class
// This is a special function called a "constructor".
// It runs automatically when a new object is created.
SimpleRobot() {
battery_percentage = 100.0;
robot_name = "Unit-01";
std::cout << robot_name << " initialized and ready!" << std::endl;
}
// A member function (a behavior)
void move(double distance) {
if (battery_percentage > 10.0) {
std::cout << robot_name << " moving " << distance << " meters." << std::endl;
battery_percentage -= 5.0; // Moving uses battery
} else {
std::cout << robot_name << " battery too low to move!" << std::endl;
}
}
// Another member function
void printStatus() {
std::cout << "--- " << robot_name << " Status ---" << std::endl;
std::cout << "Battery: " << battery_percentage << "%" << std::endl;
}
private: // Things only accessible from within the class
// Member variables (the data)
double battery_percentage;
std::string robot_name;
};
int main() {
// Create an object (an instance) of our SimpleRobot class
SimpleRobot my_bot;
my_bot.printStatus();
my_bot.move(10.0);
my_bot.printStatus();
// The line below would cause a compiler error because battery_percentage is private!
// my_bot.battery_percentage = 50.0;
// This is a good thing! It prevents accidental modification of the robot's internal state.
return 0;
}
The key difference here is the private
section. By making battery_percentage
private, we force the outside world to interact with it only through the public
functions we provide (move
, printStatus
). This principle is called encapsulation, and it’s a cornerstone of reliable software design. We’ll dive deeper into it next week.
Hands-On Example
Create a class
named Sensor
.
- Give it two
private
member variables:std::string type
(e.g., “LIDAR”, “IMU”)bool is_active
- Give it one
public
member variable:int id_number
- Write a
public
member function calledactivate()
that setsis_active
totrue
and prints a message like “Sensor 12 is now active.” - Write a
public
member function calleddeactivate()
that setsis_active
tofalse
. - Write a
public
member function calledprintStatus()
that prints the sensor’s type and whether it is active. - In
main()
, create an object of yourSensor
class, set its ID and type (you’ll need a way to do this… maybe a constructor?), activate it, and print its status.
Lesson 9: Object-Oriented Programming (OOP) Part 1: Encapsulation & Abstraction
Learning Objectives
- Deeply understand the principle of Encapsulation and its benefits.
- Implement “getter” and “setter” methods to provide controlled access to private data.
- Understand Abstraction as the concept of hiding complex implementation details behind a simple interface.
Key Vocabulary
- Encapsulation: The bundling of data (attributes) and the methods (functions) that operate on that data into a single unit (a class), and restricting direct access to some of the object’s components.
- Abstraction: The concept of hiding the complex reality while exposing only the necessary parts. It’s about simplifying the user’s interaction with a complex system.
- Interface: The set of
public
methods a class provides. It’s the “contract” the class has with the outside world. - Getter: A public method used to retrieve the value of a private member variable. (e.g.,
getBatteryLevel()
) - Setter: A public method used to modify the value of a private member variable, often including validation logic. (e.g.,
setSpeed(int speed)
)
Lesson Content
Last week, we introduced classes and the private
keyword. Today, we’re diving into why that’s so important. The two concepts we’re covering, Encapsulation and Abstraction, are the pillars that make OOP so effective for building complex systems like robots.
Encapsulation: The Protective Shell
Encapsulation is about creating a protective shell around your data. You make your member variables private
and provide carefully controlled public
methods for interacting with them.
Why bother?
- Validity: You can prevent invalid data from being set. A motor speed can’t be negative. A battery percentage can’t be 200%. A “setter” method can enforce these rules.
- Simplicity: It hides the internal complexity. Maybe storing the robot’s
angle
involves a complex calculation with quaternions, but the user of your class can just call a simplegetAngle()
and not worry about the math. - Maintainability: If you decide to change how you store data internally (e.g., change from degrees to radians), you only need to update the code inside the class. All the code outside the class that uses your public methods doesn’t have to change at all!
Let’s improve our robot class with proper getters and setters.
#include <iostream>
class MotorController {
public:
MotorController() {
speed = 0; // Default speed is 0
std::cout << "Motor controller initialized." << std::endl;
}
// A "setter" for the speed
void setSpeed(int new_speed) {
// Validation logic!
if (new_speed < -255) {
speed = -255;
std::cout << "Warning: Speed clamped to -255 (max reverse)." << std::endl;
} else if (new_speed > 255) {
speed = 255;
std::cout << "Warning: Speed clamped to 255 (max forward)." << std::endl;
} else {
speed = new_speed;
}
std::cout << "Motor speed set to " << speed << std::endl;
}
// A "getter" for the speed
int getSpeed() {
return speed;
}
private:
int speed; // Range: -255 to 255
};
int main() {
MotorController left_motor;
left_motor.setSpeed(150);
std::cout << "Current speed is: " << left_motor.getSpeed() << std::endl;
left_motor.setSpeed(500); // Try to set an invalid speed
std::cout << "Current speed is: " << left_motor.getSpeed() << std::endl;
return 0;
}
We can no longer set speed
directly. We must go through setSpeed()
, which protects the motor from invalid commands.
Abstraction: The Simple Interface
Abstraction is the natural result of good encapsulation. Think about driving a car. You have a simple interface: a steering wheel, pedals, a gear stick. You don’t need to know how the fuel injection system, the transmission, or the anti-lock brakes actually work. All that complexity is abstracted away.
In our MotorController
class, the setSpeed()
and getSpeed()
methods are the interface. The user of this class doesn’t need to know if the speed is stored as an int
, a double
, or if it controls a PWM signal on a specific hardware pin. They just know they can call setSpeed()
and the motor will behave as expected. This makes it incredibly easy to use your code and build larger systems from these simple, reliable components.
Hands-On Example
Create a class
named Battery
.
- It should have one
private
member variable:double charge_level
(from 0.0 to 100.0). - In the constructor, initialize
charge_level
to100.0
. - Create a
public
gettergetChargeLevel()
that returns the current charge. - Create a
public
methodvoid drain(double amount)
. This should decrease thecharge_level
, but make sure it never goes below 0. - Create a
public
methodvoid charge(double amount)
. This should increase thecharge_level
, but make sure it never goes above 100. - In
main()
, create aBattery
object. Drain it by some amount, charge it, try to over-drain it, and print the level after each action to verify your logic is working.
Lesson 10: OOP Part 2: Inheritance & Polymorphism (Creating Robot Hierarchies)
Learning Objectives
- Use Inheritance to create a new class (derived class) from an existing class (base class).
- Understand the concept of Polymorphism and use
virtual
functions to achieve it. - Design a hierarchy of related robot classes that share common functionality.
Key Vocabulary
- Inheritance: A mechanism in OOP where a new class (derived or child class) inherits properties and behaviors (members) from an existing class (base or parent class). It represents an “is-a” relationship (e.g., a
Drone
is aRobot
). - Base Class (Parent): The class being inherited from.
- Derived Class (Child): The class that inherits from another class.
- Polymorphism: From Greek, meaning “many forms.” In C++, it’s the ability of an object to be treated as an instance of its parent class, but to invoke the child class’s implementation of a method.
virtual
: A keyword used in a base class function declaration to indicate that the function can be overridden by a function with the same signature in a derived class.- Override: To provide a specific implementation of a virtual function in a derived class.
Lesson Content
Not all robots are the same, but many share common features. A drone, a rover, and a robot arm are all “robots.” They might all have a status()
and a recharge()
method. Inheritance allows us to capture this “is-a” relationship in code.
Inheritance: Building on What Exists
We can define a generic Robot
base class that contains all the common attributes and behaviors. Then, we can create more specific classes like Rover
and Drone
that inherit from Robot
. They get all the Robot
functionality for free and can add their own specializations.
#include <iostream>
#include <string>
// --- The Base Class ---
class Robot {
public:
Robot(std::string name) {
this->robot_name = name;
this->battery_level = 100.0;
}
void recharge() {
battery_level = 100.0;
std::cout << robot_name << " is fully recharged." << std::endl;
}
void printStatus() {
std::cout << "[" << robot_name << "] Battery: " << battery_level << "%" << std::endl;
}
protected: // Like private, but accessible to derived (child) classes
std::string robot_name;
double battery_level;
};
// --- The Derived Classes ---
// The colon ':' signifies inheritance. "Rover inherits from Robot"
class Rover : public Robot {
public:
// Rover's constructor calls the Robot's constructor
Rover(std::string name) : Robot(name) {}
void drive(double distance) {
std::cout << robot_name << " is driving " << distance << "m on the ground." << std::endl;
battery_level -= distance * 0.5; // Rovers are pretty efficient
}
};
class Drone : public Robot {
public:
Drone(std::string name) : Robot(name) {}
void fly(double distance) {
std::cout << robot_name << " is flying " << distance << "m in the air." << std::endl;
battery_level -= distance * 2.0; // Drones use a lot more power
}
};
int main() {
Rover mars_rover("Curiosity");
Drone quadcopter("Skydio");
mars_rover.printStatus();
mars_rover.drive(50.0);
mars_rover.printStatus();
mars_rover.recharge(); // This method was inherited from Robot!
quadcopter.printStatus();
quadcopter.fly(50.0); // Drones and Rovers have different move methods
quadcopter.printStatus();
return 0;
}
Polymorphism: Same Action, Different Behavior
This is where things get really powerful. Let’s say every robot needs a move()
method. But how a robot moves is specific to its type (a rover drives, a drone flies).
We can declare a virtual
function in the base class. This is like a placeholder. Each child class can then override this function with its own specific implementation.
#include <iostream>
#include <string>
#include <vector>
class Robot {
public:
Robot(std::string name) : robot_name(name) {}
// A virtual function. The "= 0" makes it a "pure virtual" function,
// meaning derived classes MUST implement it.
virtual void move(double distance) = 0;
std::string getName() { return robot_name; }
protected:
std::string robot_name;
};
class Rover : public Robot {
public:
Rover(std::string name) : Robot(name) {}
// Override the virtual function from the base class
void move(double distance) override {
std::cout << robot_name << " (Rover) drives " << distance << "m." << std::endl;
}
};
class Drone : public Robot {
public:
Drone(std::string name) : Robot(name) {}
void move(double distance) override {
std::cout << robot_name << " (Drone) flies " << distance << "m." << std::endl;
}
};
int main() {
Rover r1("Curiosity");
Drone d1("Ingenuity");
// The magic of polymorphism: we can store pointers to different
// derived objects in a vector of base class pointers!
std::vector<Robot*> fleet;
fleet.push_back(&r1);
fleet.push_back(&d1);
// Now we can treat them all as generic "Robots"
for (Robot* bot_ptr : fleet) {
std::cout << "Commanding " << bot_ptr->getName() << " to move." << std::endl;
// The correct version of move() is called automatically!
// This is determined at runtime.
bot_ptr->move(20.0);
}
return 0;
}
In the main
function’s loop, the code doesn’t know or care if the bot_ptr
is pointing to a Rover
or a Drone
. It just calls move()
. C++ automatically figures out at runtime which version of the function to execute. This allows you to write incredibly flexible and extensible systems. You could add a Submarine
class tomorrow, and the main loop would work without any changes!
Hands-On Example
- Create a base class called
Sensor
with aprivate
std::string sensor_type
and apublic
pure virtual functionvirtual void takeReading() = 0;
. - Create two derived classes:
TemperatureSensor
andPressureSensor
. - Each derived class should have a constructor that sets the
sensor_type
appropriately. - Implement the
takeReading()
method in each derived class.TemperatureSensor
should print something like “Reading temperature: 25.3 C”.PressureSensor
should print something like “Reading pressure: 101.2 kPa”.
- In
main()
, create astd::vector
ofSensor*
pointers and add one of each type of sensor to it. - Loop through the vector and call
takeReading()
on each sensor to demonstrate polymorphism.
Lesson 11: The Standard Template Library (STL): Your Robotics Toolkit
Learning Objectives
- Understand the purpose of the STL as a collection of pre-built, efficient components.
- Use common STL containers like
std::vector
,std::map
, andstd::queue
. - Utilize STL algorithms like
std::sort
andstd::find
.
Key Vocabulary
- STL (Standard Template Library): A library of C++ template classes that provides common programming data structures and functions such as lists, stacks, arrays, etc.
- Container: An STL class designed to store a collection of objects (e.g.,
std::vector
,std::map
). - Iterator: An object that acts like a pointer, used to “point to” and traverse the elements of a container.
- Algorithm: A function from the STL that performs a common operation on a range of elements, such as sorting, searching, or counting.
std::map
: An associative container that stores elements in a mapped fashion. Each element has a key value and a mapped value. No two mapped values can have the same key values.
Lesson Content
Why reinvent the wheel? The C++ STL is a treasure trove of robust, highly optimized, and battle-tested tools. For a roboticist, it provides the essential building blocks for almost any data management task. We’ve already used std::vector
, but that’s just scratching the surface.
Containers: The Right Box for the Job
Choosing the right container can have a huge impact on your program’s performance and clarity.
std::vector
: The go-to default. A dynamic array. Fast access to elements by index (my_vec[i]
). Good for storing sequences of sensor readings or waypoints.std::map
: A dictionary or key-value store. Perfect for associating a name with a value, like storing robot configuration parameters.map["motor_port"] = 3;
std::queue
: A “first-in, first-out” (FIFO) data structure. Imagine a queue of commands for your robot. The first command youpush
is the first one youpop
. This ensures tasks are executed in the order they were received.
Let’s see a std::map
in action for robot configuration.
#include <iostream>
#include <string>
#include <map>
int main() {
// The map stores string keys and double values
std::map<std::string, double> config;
// Add key-value pairs
config["max_speed_mps"] = 2.5;
config["lidar_range_m"] = 12.0;
config["camera_fov_deg"] = 60.0;
// Access a value by its key
std::cout << "Max speed is: " << config["max_speed_mps"] << " m/s" << std::endl;
// Check if a key exists
if (config.count("imu_update_rate_hz")) {
std::cout << "IMU rate is: " << config["imu_update_rate_hz"] << std::endl;
} else {
std::cout << "IMU update rate not specified in config." << std::endl;
}
// Iterate over all key-value pairs
for (const auto& pair : config) {
std::cout << "- " << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
Algorithms: Powerful Actions on Your Data
The STL provides dozens of algorithms that work on containers. You need to #include <algorithm>
.
Imagine you have a vector of distance readings and you need to find the closest obstacle.
#include <iostream>
#include <vector>
#include <algorithm> // For std::sort and std::min_element
int main() {
std::vector<double> distances = {4.5, 2.1, 8.0, 1.2, 3.4};
// --- Sorting ---
// std::sort modifies the vector in-place
std::sort(distances.begin(), distances.end());
std::cout << "Sorted distances: ";
for (double d : distances) {
std::cout << d << " ";
}
std::cout << std::endl;
// The smallest element is now at the front
std::cout << "Closest obstacle (after sort): " << distances[0] << "m" << std::endl;
// --- Finding the minimum without sorting ---
// std::min_element returns an "iterator" (like a pointer) to the smallest element.
// We use * to dereference it and get the value.
auto min_it = std::min_element(distances.begin(), distances.end());
std::cout << "Closest obstacle (using min_element): " << *min_it << "m" << std::endl;
return 0;
}
Using STL algorithms is almost always better than writing your own loops. They are less error-prone and often much faster because the library implementers have spent years optimizing them.
Hands-On Example
You are processing a list of tasks for a robot, each with a priority level.
- Create a
std::vector
ofstd::pair<int, std::string>
. Astd::pair
is a simple struct that holds two values. Theint
will be the priority (lower number is higher priority) and thestring
will be the task description. - Add a few tasks out of order, for example:
(3, "Patrol hallway")
,(1, "Recharge battery")
,(2, "Inspect machinery")
. - Use
std::sort
to sort the vector. By default, it will sort pairs based on their first element (the priority number). - Loop through the sorted vector and print the tasks in order of priority, like:
Executing task with priority 1: Recharge battery Executing task with priority 2: Inspect machinery ...
Lesson 12: File I/O & Data Logging: Recording a Robot’s Journey
Learning Objectives
- Read data from a text file into your program using
std::ifstream
. - Write data from your program to a text file using
std::ofstream
. - Implement a simple data logging system for a robot’s sensor values.
Key Vocabulary
- I/O: Input/Output. The communication between a program and the outside world (e.g., files, the console).
- Stream: A sequence of data flowing from a source to a destination.
std::cout
is an output stream to the console. File streams work similarly. std::ifstream
: Input File Stream. A class for reading from files.std::ofstream
: Output File Stream. A class for writing to files.- CSV (Comma-Separated Values): A common text file format for storing tabular data, where each line is a record and values are separated by commas. It’s easy to open in spreadsheet software.
Lesson Content
A robot that can’t remember what it did or what it saw is severely limited. Data logging is essential for debugging, analysis, and mission playback. If your robot failed, you need to look at its sensor logs from the moments before the failure to understand why. C++ provides a powerful and straightforward way to handle files using streams.
Writing to a File with std::ofstream
Let’s log some simulated sensor data to a CSV file. The process is very similar to using std::cout
.
#include <iostream>
#include <fstream> // File Stream header
#include <string>
int main() {
// 1. Create an ofstream object.
// The constructor takes the filename. This will create the file if it doesn't exist,
// or overwrite it if it does.
std::ofstream log_file("robot_log.csv");
// 2. Check if the file was opened successfully.
if (!log_file.is_open()) {
std::cerr << "Error: Could not open log file for writing." << std::endl;
return 1; // Return a non-zero value to indicate an error
}
// 3. Write to the file just like you write to std::cout.
// Let's write a header row for our CSV.
log_file << "Timestamp,PosX,PosY,Battery\n";
// Now let's log some data points.
log_file << "1.0,5.2,3.1,98.5\n";
log_file << "2.0,5.8,3.2,97.0\n";
log_file << "3.0,6.3,3.3,95.5\n";
// 4. Close the file when you're done.
// This happens automatically when the ofstream object goes out of scope,
// but it's good practice to do it explicitly.
log_file.close();
std::cout << "Data successfully written to robot_log.csv" << std::endl;
return 0;
}
After running this, you’ll find a new file named robot_log.csv
in the same directory. Open it with a text editor or a spreadsheet program!
Reading from a File with std::ifstream
Now let’s do the reverse. We’ll read a mission file that contains a list of waypoints for our robot to follow.
First, create a text file named waypoints.txt
with this content:
10.0 5.0
12.5 8.0
12.5 12.0
8.0 10.0
Now, the C++ code to read it:
#include <iostream>
#include <fstream>
#include <vector>
struct Point {
double x, y;
};
int main() {
// 1. Create an ifstream object.
std::ifstream waypoint_file("waypoints.txt");
std::vector<Point> path;
// 2. Check if the file opened.
if (!waypoint_file.is_open()) {
std::cerr << "Error: Could not open waypoint file." << std::endl;
return 1;
}
// 3. Read from the file.
// The stream extraction operator >> is smart. It reads space-separated values.
double temp_x, temp_y;
while (waypoint_file >> temp_x >> temp_y) {
// This loop continues as long as we can successfully read two doubles.
path.push_back({temp_x, temp_y});
}
// 4. Close the file.
waypoint_file.close();
// Now let's verify we read the data correctly.
std::cout << "Path loaded with " << path.size() << " waypoints:" << std::endl;
for (const auto& p : path) {
std::cout << "- (X=" << p.x << ", Y=" << p.y << ")" << std::endl;
}
return 0;
}
Hands-On Example
Create a robot configuration loader.
- Create a text file named
config.txt
. In it, put key-value pairs, one per line, separated by a space. For example:robot_name Unit-Alpha max_velocity 1.5 has_gripper true
- In C++, create a
std::map<std::string, std::string>
to store the configuration. - Write code that opens
config.txt
and reads it line by line. - Inside the loop, read the key (the first word) and the value (the second word) from each line and insert them into your map.
- After the loop, print the contents of the map to verify that your configuration was loaded correctly.
Lesson 13: Concurrency & Multithreading: Running Multiple Processes at Once
Learning Objectives
- Understand why concurrency is essential for modern robotics.
- Launch a separate thread of execution using
std::thread
. - Use
std::mutex
to prevent race conditions when multiple threads access shared data.
Key Vocabulary
- Concurrency: The concept of multiple tasks running in overlapping time periods. It doesn’t necessarily mean they run at the exact same instant (that’s parallelism), but that their execution is interleaved.
- Thread: An independent sequence of execution within a program. A single program can have multiple threads running concurrently.
std::thread
: The C++ STL class for creating and managing threads.- Race Condition: A bug that occurs when the output of a system depends on the sequence or timing of uncontrollable events, such as the scheduling of threads. It often happens when multiple threads try to modify the same data at the same time.
std::mutex
(Mutual Exclusion): A synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads. Only one thread can “lock” the mutex at a time.
Lesson Content
A real robot is doing many things at once. It’s driving its motors, reading from a LIDAR, processing camera images, and communicating with a base station. If you tried to do all of this sequentially in a single loop, your robot would be incredibly slow and unresponsive. It couldn’t drive while looking for obstacles!
Concurrency is the answer. We can run these tasks in separate threads. Think of your main()
function as the main thread. You can launch other threads to handle background tasks.
Launching a Thread with std::thread
Let’s create a simple program where the main thread does some work, while a background thread continuously “monitors the battery.”
#include <iostream>
#include <thread> // Header for threading
#include <chrono> // Header for time-related utilities
// This function will be executed by our new thread.
void batteryMonitor() {
for (int i = 0; i < 5; ++i) {
std::cout << "[Monitor Thread] Battery is OK." << std::endl;
// Tell this thread to sleep for 1 second.
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main() {
std::cout << "[Main Thread] Starting main task." << std::endl;
// Create a thread object. The constructor takes the function to run.
std::thread monitor_thread(batteryMonitor);
// The main thread can continue doing its own work.
for (int i = 0; i < 3; ++i) {
std::cout << "[Main Thread] Executing primary control loop..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
// IMPORTANT: We must wait for the other thread to finish before main exits.
// This is called "joining" the thread.
monitor_thread.join();
std::cout << "[Main Thread] All tasks complete. Shutting down." << std::endl;
return 0;
}
Compile this with an extra flag: g++ my_thread_program.cpp -o my_thread_program -pthread
. When you run it, you’ll see the output from both threads interleaved!
The Danger: Race Conditions and std::mutex
What happens when two threads need to access the same variable? This is where bugs hide.
Imagine a global variable int command_counter = 0;
. One thread reads it, adds one, and writes it back. Another thread does the same. If their timing is just right, they might both read the value 5
, both calculate 6
, and both write 6
back. The counter only incremented once when it should have incremented twice! This is a race condition.
A std::mutex
is like a talking stick. To access the shared data, a thread must first lock()
the mutex. While it’s locked, any other thread that tries to lock it will be forced to wait. When the first thread is done, it unlock()
s the mutex, allowing another thread to take its turn.
#include <iostream>
#include <thread>
#include <vector>
#include <mutex> // Header for mutex
std::mutex mtx; // The mutex that will protect our shared data
int task_count = 0; // The shared data
void processTask() {
for (int i = 0; i < 10000; ++i) {
// Lock the mutex before touching shared data
mtx.lock();
task_count++;
// Unlock it immediately after
mtx.unlock();
}
}
int main() {
std::thread t1(processTask);
std::thread t2(processTask);
t1.join();
t2.join();
// Without the mutex, this value would be unpredictable and almost never 20000.
// With the mutex, it will be exactly 20000 every time.
std::cout << "Total tasks processed: " << task_count << std::endl;
return 0;
}
In modern C++, it’s even better to use a std::lock_guard
which automatically locks on creation and unlocks when it goes out of scope. This prevents you from forgetting to unlock the mutex.
Hands-On Example
Create a program that simulates a robot driving while a separate thread logs its position.
- Create a global
struct Pose { double x; double y; };
and a globalstd::mutex
. - Write a
logger
function that will run in its own thread. It should loop 10 times. In each loop, it should lock the mutex, read the global pose, print it to the console, unlock the mutex, and then sleep for 500 milliseconds. - In
main()
, create a globalPose
variable. Launch thelogger
thread. - Then, in the
main
thread, loop 5 times. In each loop, lock the mutex, update the robot’sx
andy
position (e.g., add 1.0 tox
), unlock the mutex, and sleep for 1 second. - Join the logger thread before
main
exits. Observe the interleaved output.
Lesson 14: Introduction to ROS Concepts with C++
Learning Objectives
- Understand the conceptual role of ROS (Robot Operating System) as a middleware.
- Learn the core ROS concepts: Nodes, Topics, Publishers, and Subscribers.
- See how the C++ skills you’ve learned map directly onto writing a simple ROS node.
Key Vocabulary
- ROS (Robot Operating System): A flexible framework for writing robot software. It is a collection of tools, libraries, and conventions that aim to simplify the task of creating complex and robust robot behavior across a wide variety of robotic platforms.
- Middleware: Software that acts as a bridge between an operating system and the applications on it. ROS handles the complex communication between different parts of your robot so you don’t have to.
- Node: A process that performs computation. A robot control system is usually comprised of many nodes. For example, one node for camera processing, one for motor control, one for path planning.
- Topic: A named bus over which nodes exchange messages. A node can “publish” messages to a topic or “subscribe” to a topic to receive messages.
- Message: A simple data structure, like a C++ struct, used for communicating on a topic. ROS has many standard message types (e.g., for laser scans, odometry, images).
- Publisher: A node that sends data out over a topic.
- Subscriber: A node that receives data by “listening” to a topic.
Lesson Content
You’ve now mastered the C++ language skills needed for robotics. The final step is to understand how these skills fit into the broader robotics software ecosystem. The most dominant framework in this space is ROS.
ROS is NOT an Operating System
Despite its name, ROS is not a traditional OS like Windows or Linux. It’s a middleware. It sits on top of an OS (usually Ubuntu Linux) and provides three key things:
- Plumbing: A structured way for different programs (nodes) to communicate with each other, whether they are on the same computer or on different computers across a network.
- Tools: A rich set of tools for debugging, visualizing, and simulating your robot.
- Capabilities: A huge ecosystem of ready-to-use packages for things like navigation, manipulation, and perception.
The Core Concepts: Nodes and Topics
Imagine a simple obstacle-avoiding robot. In ROS, you would break this down into separate, single-purpose programs called Nodes.
/lidar_driver
Node: This node’s only job is to talk to the physical LIDAR hardware and publish its distance readings onto a Topic called/scan
./obstacle_avoidance
Node: This node doesn’t know or care about the LIDAR hardware. It simply subscribes to the/scan
topic. Whenever a new message appears, it runs its logic. If it detects an obstacle, it publishes a command message (e.g.,{linear: 0.0, angular: 0.5}
) onto a Topic called/cmd_vel
(command velocity)./motor_controller
Node: This node subscribes to the/cmd_vel
topic. When it receives a velocity command, its only job is to translate that command into the correct electrical signals for the robot’s motors.
This “decoupled” architecture is incredibly powerful. You can replace the LIDAR with a camera just by creating a new camera node that also publishes to the /scan
topic. The /obstacle_avoidance
node doesn’t need to change at all! You can also easily inspect the data on any topic for debugging using ROS tools.
How C++ Fits In
Each of these nodes is a separate C++ program. The C++ you’ve learned is what you use to write the logic inside each node. The ROS client library (roscpp
) provides the classes and functions to handle the communication part.
Here’s a conceptual pseudo-code for a C++ subscriber node. This isn’t runnable code without a full ROS environment, but it shows how our skills map directly.
// This is pseudo-code to illustrate the concept.
#include "ros/ros.h" // The main ROS C++ header
#include "sensor_msgs/LaserScan.h" // A standard ROS message type
// This is a "callback function". ROS will call this function for us
// whenever a new message arrives on the topic we're subscribed to.
void laserScanCallback(const sensor_msgs::LaserScan::ConstPtr& msg) {
// 'msg' is a pointer to the received laser scan message.
// The LaserScan message type has a vector of ranges.
// Let's find the minimum range to find the closest obstacle.
float min_distance = msg->ranges[0];
for (float range : msg->ranges) {
if (range < min_distance) {
min_distance = range;
}
}
ROS_INFO("Closest obstacle is at: %f meters", min_distance);
}
int main(int argc, char **argv) {
// Initialize the ROS node
ros::init(argc, argv, "obstacle_detector_node");
// Create a NodeHandle, our main access point to ROS communications
ros::NodeHandle nh;
// Create a subscriber object.
// We tell it to subscribe to the "/scan" topic.
// We tell it the message type is LaserScan.
// We give it the name of our callback function.
ros::Subscriber sub = nh.subscribe("/scan", 1000, laserScanCallback);
// ros::spin() enters a loop, waiting for messages to arrive.
// When a message arrives, it calls the appropriate callback function.
ros::spin();
return 0;
}
Look at the skills used here: functions (laserScanCallback
), loops (for
), variables (min_distance
), and the overall program structure with main()
. All the hard C++ work you’ve done is the prerequisite for writing powerful ROS nodes like this.
Hands-On Example
This week’s example is conceptual, as it requires a full ROS installation. Your task is to design a ROS system on paper.
Problem: Design the nodes and topics for a robot that picks up a can from a table.
- List the Nodes: What distinct processes would you need? Think about vision, arm control, gripper control, and overall logic. Give each a name (e.g.,
/vision_processor
). - List the Topics: What information needs to be passed between these nodes? What are the data types? Give each topic a name (e.g.,
/can_position
). - Draw a Diagram: Draw boxes for your nodes and ovals for your topics. Draw arrows from the publishing nodes to the topics, and from the topics to the subscribing nodes. This diagram is the fundamental blueprint for a ROS application.
Final Project (Weeks 15-16): Autonomous Rover Navigation Simulator
Project Description
Congratulations on making it to the final project! It’s time to integrate everything you’ve learned. You will build a text-based simulation of an autonomous rover navigating a 2D grid world. The rover will be given a map with obstacles and a target destination. Your C++ program will need to load the map, plan a path from the start to the target while avoiding obstacles, and then execute the path, printing the rover’s status at each step.
This project will test your skills in file I/O, classes (OOP), STL containers (vector
, queue
), algorithms, and logical decision-making.
Step-by-Step Instructions
Milestone 1: The World and the Rover (Lessons 8, 9, 11)
- Create a
Map
class.- This class will be responsible for loading and representing the environment.
- It should have a
private
member:std::vector<std::string> grid;
to store the map layout. - It should have a
public
methodbool loadMapFromFile(const std::string& filename);
. This method will read a text file (see format below) into thegrid
. - It should have a
public
methodvoid printMap();
to display the current state of the grid to the console. - It should have a helper method like
bool isObstacle(int x, int y);
that returns true if the specified coordinate is an obstacle.
- Create a
Rover
class.- It should have
private
members for its current position:int x;
,int y;
. - It should have a constructor that sets the initial
x
andy
position. - It should have public getters
int getX()
andint getY()
.
- It should have
- Map File Format: Create a file
map.txt
. It’s a simple grid where ‘#’ represents an obstacle, ‘.’ is open space, ‘S’ is the start, and ‘T’ is the target.########## #S.......# #..###...# #.#.T.#..# #.#...#..# #...#....# ##########
Milestone 2: Pathfinding with Breadth-First Search (BFS) (Lessons 4, 6, 11)
This is the most challenging and rewarding part. We will implement the BFS algorithm to find the shortest path.
- Create a
Pathfinder
class.- It will have a public method:
std::vector<Point> findPath(const Map& map, const Point& start, const Point& target);
. (Point
can be a simplestruct
withint x, y;
).
- It will have a public method:
- BFS Algorithm Logic (inside
findPath
):- You will need a
std::queue<Point>
to store the “frontier” of nodes to visit. - You will need a
std::vector<std::vector<bool>> visited
grid of the same size as the map to keep track of where you’ve already been. - You will need a way to reconstruct the path once you find the target. A
std::map<Point, Point> parent_map
is a good way to do this, where you storeparent_map[child] = parent
. - The Algorithm: a. Add the
start
point to the queue. b. While the queue is not empty: c. Dequeue the current point. d. If the current point is thetarget
, you’re done! Reconstruct the path using yourparent_map
and return it. e. Otherwise, for each neighbor (up, down, left, right) of the current point: f. If the neighbor is within map bounds, is not an obstacle, and has not been visited: g. Mark it as visited, record its parent, and enqueue it. - If the queue becomes empty and you haven’t found the target, it means no path exists. Return an empty vector.
- You will need a
Milestone 3: Simulation and Execution (Lessons 3, 5, 12)
- Integrate Everything in
main()
.- Create a
Map
object and loadmap.txt
. - Find the ‘S’ and ‘T’ coordinates from the map.
- Create a
Rover
object at the start position. - Create a
Pathfinder
object and callfindPath()
. - If a path is found: a. Loop through the points in the returned path. b. In each step of the loop: i. Update the rover’s position on the map grid (e.g., change the old spot to ‘.’ and the new spot to ‘R’ for Rover). ii. Clear the console and print the updated map. iii. Print the rover’s current status (e.g., “Moved to (X, Y)”). iv. Add a small delay (
std::this_thread::sleep_for
) to make the simulation viewable. - If no path is found, print a “Target is unreachable” message.
- Create a
- Final Output: Your program, when run, should display the map evolving frame-by-frame as the rover (‘R’) moves along the calculated path from ‘S’ to ‘T’.
Good luck, and have fun building! This project is a significant achievement and a fantastic demonstration of your new skills in C++ for Robotics.