Writing Classes
Defining a C++ class with a header, source file, and usage example
A class groups related data and functions into a type you can use in your program. An object is one instance of that type: a Motor class describes what a motor stores and does, while leftMotor and rightMotor are separate objects with their own values.
Here is a small Motor class that stores a name and a requested speed. It does not communicate with hardware.
Writing the header file
The header describes the class so other source files can use it. The source file implements the functions declared in the header.
#ifndef TRICKFIRE_EXAMPLE_MOTOR_H
#define TRICKFIRE_EXAMPLE_MOTOR_H
#include <string>
class Motor {
public:
explicit Motor(const std::string& name);
void setSpeed(double speed);
void stop();
double getSpeed() const;
private:
std::string name;
double speed;
};
#endifHeader guards and includes
The #ifndef, #define, and #endif lines form a header guard. They are required at the start of each header file.
Include the headers needed by the types your header uses. Since Motor contains a std::string, motor.h includes <string> itself.
Public and private members
Public and private members are the same concept as you may have learned about in Java.
publicmembers form the interface other code can use, such assetSpeed().privatemembers are internal data or helpers. Code outside the class cannot access them directly.
Constructors and member functions
A constructor initializes an object. It has the same name as the class and no return type. explicit prevents this single-argument constructor from being used for an implicit conversion to Motor.
The parameter const std::string& name accepts a string by reference without allowing this function to modify it through that reference. Our constructor copies it into name, so the object owns its stored name.
Member functions declare a return type, name, and parameters. void means that setSpeed() and stop() return no value, while getSpeed() returns a double. The const after getSpeed() means it can be called on a const object and cannot modify ordinary member variables through that call.
Writing the source file
#include "motor.h"
Motor::Motor(const std::string& name)
: name{name} {
}
// alternate constructor syntax
// Motor::motor(const std::string& name) {
// this->name = name;
// }
void Motor::setSpeed(double speed) {
speed = speed;
}
void Motor::stop() {
speed = 0.0;
}
double Motor::getSpeed() const {
return speed;
}Include your own header so the compiler can check that these implementations match its declarations. Use quotes for project headers, such as "motor.h", and angle brackets for standard library headers, such as <string>.
Motor:: identifies each function as a member of Motor. The constructor's member initializer list, : name{name}, initializes name before the constructor body runs. speed uses the default member initializer of 0.0 from the header.
Keep signatures consistent between the two files, including parameter types and the trailing const. For example, defining double Motor::getSpeed() without const would not match the declaration in this header.
Using the class
#include "motor.h"
#include <iostream>
int main() {
Motor leftMotor{"left"};
Motor rightMotor{"right"};
leftMotor.setSpeed(0.5);
rightMotor.setSpeed(0.25);
std::cout << "Left speed: " << leftMotor.getSpeed() << '\n';
std::cout << "Right speed: " << rightMotor.getSpeed() << '\n';
leftMotor.stop();
std::cout << "Left after stopping: " << leftMotor.getSpeed() << '\n';
return 0;
}Use the dot operator to call a public function on an object. Each object stores its own name and speed, so changing leftMotor does not change rightMotor.
Templates
Templates are useful feature when programming. They allow you to create a container class or a function with a generic type.
Here is a small example of a template function
template <typename T>
T larger(T a, T b) {
// this can be read as if a is greater than b, then return a, else return b
return (a > b) ? a : b;
}If the call larger(3 ,5) was made, the function would not strictly need to be a template function as the parameters, 3 and 5, are both int types. If you wanted to compare the
size of an int and a double, for example, then you would have to make the call larger<double>(3, 5.5). This does not quite mean that any object can be passed in as an argument to
a template function, however. The above calls work because ints can be implicitly type converted to doubles.
Relating templates to ROS
When writing a ROS node in C++ (rclcpp), you will encounter templates, specifically when writing subscribers and publishsers. For reference, here is an example. More can be found in the ROS docs.
m_subscriber = this->create_subscription<ExampleMsg>("PythonPublisher", 10,
functionLambda);In this example, the template class is located within the angle brackets ("< >"). This means that the message type subcribers and publishsers can be specified to whatever is required, assuming you have declared it. For example, the type "ExampleMsg" can be replaced with std_msgs::msg::String or geometry_msgs::msg::PoseStamped.