- Software Division
- Software Division Home
- Tutorials
[ Software Division : WATO ROS C++ Code Style ]
Created by [ Henry Wang], last modified on Feb 04, 2020
This style guide is partly based on the ROS C++ Style
Guide and Google’s
C++ Style
Guide.
It is required that all members adhere to this guide when developing C++
code, and that all leads and reviewers compare code against these
guidelines when performing code reviews. If an item is not specified in
this document, please consult the ROS and Google style guides linked
above (ROS first, Google second).
Section 1: File System Components
1.0 Directory Structure
Your git repositories should be cloned into the src folder of your
catkin workspace. Any ROS packages that you build should be created in
your sub-team’s folder. For example:
catkin_workspace/
build/
devel/
src/
team_folder/
sub_team_folder/
package_1/
package_2/
Inside each package folder you should have:
- CMakeLists.txt
- package.xml
- An include directory, which contains a package_name directory
- A src directory
- Other directories depending on your package, e.g. msg, launch, srv,
test …
The include/package_name directory should contain .h files and the src
directory should contain .cpp files. Any .cpp file that implements a
class should have a corresponding .h file. See another section for how
to write your .h and .cpp files.
1.1 File Naming
- Directories should be named like_this/
- Header and implementation files should be named like_this.h and
like_this.cpp
- CMakeLists.txt and package.xml must be kept with their default names
- ROS packages should be named like_this
- ROS message and service files should be named LikeThis.msg and
LikeThis.srv (note that this refers to the file names; the names
of actual topics and services have a different convention, described
in 2.0 Naming Conventions)
2.0 Naming Conventions
ROS topic names, service names, node names, and frame names: like_this
Libraries (such as in your CMakeLists.txt): like_this
C++ classes and structs
- class MyClass
- Enumerations are custom types and therefore should be named like
classes:
enum MyEnum
Variables
- int my_variable = 0;
- Variables that are class members should have a trailing underscore
(e.g. double my_class_member_;)
- Struct members are named without this underscore
- Variable names must be descriptive; avoid abbreviations unless
intuitive
- Function parameters are named in the same way as variables
- Global variables should never be used, but if you really can’t help
it (and have a VERY good reason), they are named with a preceding
g_
(int g_my_disgustingly_awful_global_variable)
Functions
- void myFunction(float my_parameter);
- Constructors and destructors are an exception, since their names are
derived from the class name
- Function names must be descriptive and intuitive to understand
Constants and Enumerators
-
const int NUM_RADARS = 6;
-
enum ObstacleType
{
STOP_SIGN = 0,
RED_LIGHT,
…
Namespaces
- namespace watonomous_util
Macros
2.1 Styling
Braces must go on the next line, and have a line to themselves
Keep the length of each line to be less than 100 characters
All code must be indented 2 additional spaces to illustrate scope
Add spaces between arithmetic and logical operators
Add spaces after decision-making keywords
while (true) // Good
while(true) // Bad
One-line decisions may omit braces if it is still readable
CORRECT:
for (int i = 0; i < NUM_PEOPLE_TO_FEED; ++i) // Note ++i or i++ is fine
{
if (tim_hortons->closed() || tim_hortons->line_length >= 20)
{
food = campus_pizza->getFood();
}
else
{
food = tim_hortons->getFood();
}
person[i].eat(food);
}
\
INCORRECT:
for(int i=0 ; i<NUM_PEOPLE_TO_FEED ; ++i) { // Poor spacing and braces
if (tim_hortons->closed() || tim_hortons->line_length>=20) // Spacing
{
food=campus_pizza->getFood(); // Bad indentation
} // Misaligned braces
else // Why would you do this
person[i].eat(food);
}
\
ACCEPTABLE:
for (int i = 0; i < NUM_PEOPLE_TO_FEED; i++) // Note ++i or i++ is fine
{
if (tim_hortons->closed() || tim_hortons->line_length >= 20)
food = campus_pizza->getFood();
else
food = tim_hortons->getFood();
person[i].eat(food);
}
Using // or /* */ are both fine, just be consistent. Comment
thoroughly so that anyone can understand your work.
All files should have a comment at the top with the file name, author’s
name and a brief description of the file:
/**
* Name colonize_mars.h
* Author Elon Musk (emusk@uwaterloo.ca)
* Brief Creates an instance of the SpaceXRocket class and launches it
* to Mars
*/
This type of comment should be included in all C++ files (.h, .cpp) and
ROS files (.msg, .launch, .srv, ...)I
[If you are editing a file that has already been created, add your name
and email underneath the original author’s, like
so:]
* Author Original Author (oauthor@uwaterloo.ca)
* Modified by Second Author (sauthor@uwaterloo.ca)
* Third Author (tauthor@edu.uwaterloo.ca)
In your .cpp file, every member function should have a function header
with the following properties:
- Name: the name of the function
- Brief: description of the function
- Param(s): description of parameters (if any)
- Return: description of the return value (if any)
\
/**
* Name getEncoderVal
* Brief Accesses the encoder value for a specified wheel
* Param wheel_num: The index of the requested wheel
* Return double: The encoder value in RPM
* Positive for CCW, negative for CW
* Return DBL_MAX if invalid
*/
double getEncoderVal(unsigned int wheel_num)
{
if (wheel_num > 3)
return DBL_MAX;
return encoder_vals_[wheel_num];
}
Header files should be contained within an appropriate header guard:
#ifndef PACKAGE_PATH_NAME_FILE_NAME_H
#define PACKAGE_PATH_NAME_FILE_NAME_H
...
#endif // PACKAGE_PATH_NAME_FILE_NAME_H
Within these guards you should define your class, which should have the
same name as your file name. For example, a class called MyClass should
be defined in a file called my_class.h. If you are using a header file
to define utility functions only (e.g., no class implementation) then
you should contain your functions/variables in a namespace. Declare all
your #includes at the top of your header file, underneath the header
guard. The general convention is to contain libraries in <> and header
files in “”.
#include <string>
#include “my_class.h”
...
2.4 C++ Implementation Files
C++ implementation files (.cpp) should #include all appropriate headers
and libraries, even if it was already included in the corresponding
.h file. This improves readability and allows developers to see the
dependencies of the file with a glimpse.
2.5 ROS Files
2.5.0 Topics, Messages, Services, Actions
All variables defined in ROS files (e.g. .msg) should have an
accompanying comment
If you find you have several variables contained within one message
file, consider breaking it into different message types that can be
included in the main message type
- E.g. the geometry_msgs/Pose message contains a Point position and a
Quaternion orientation, which contain x, y, z and x, y, z, w
respectively. This is a better implementation than using a flat .
hierarchy of seven fields in the geometry_msgs/Pose message.
- Same idea goes for services and actions
Tips for when to use topics, services and actions:
- Topics are best suited for continuous streams of data. They are
also suitable for data that is used by a variety of nodes, or an
unknown amount of nodes (the existence of the topic is not dependent
on or blocked by the existence of publishers/subscribers)
- Services are generally used for quick calculations/procedures that
apply to specific nodes (note that services are blocking and
therefore must terminate quickly)
- Actions are used for long-running routines. Like services, actions
apply to specific nodes and do not use the many-to-many
communication supported by topics. Actions are useful when your
procedural call takes a long time to process, as they are
non-blocking and provide periodic feedback.
- If you are not sure what to use, a topic is generally safe.
2.5.1 CMakeLists.txt, package.xml
Delete all unnecessary comments that are generated by the default
CMakeLists.txt and package.xml
In package.xml:
- buildtool_depend
- build_depend
- run_depend
Section 3: Coding Practices
3.0 Classes
- In your class constructor, be certain that all of your class
variables are properly initialized and/or have default values that
make sense
- While the struct and class keywords are nearly identical in C++, we
assign our own meanings to them
- A struct should be a passive object, limited to the ability to store
information in variables, get/set said variables, and possibly
define constants
- Anything with more functionality beyond this should be a class
3.1 Inheritance
- All inheritance should be public
- Rather than using private inheritance, create an instance of the
class you want to inherit from as a class member instead
- The use of protected is allowed, however data members of a base
class should generally be kept private. If needed by a child class
then there can be appropriate getter/setter methods
- Multiple inheritance generally should not be used; often there
exists a better, cleaner implementation
- If using interface inheritance, it is useful to end the class name
with the word Interface. Classes that are not pure interfaces are
not allowed to end in the word Interface.
3.2 Friend Classes
- Friend classes are allowed, but should be used with caution.
Defining a class as a friend of another class effectively extends
the encapsulated region of a class. To ensure that this
encapsulation is easily understood, friend classes should be defined
in the same file, and used within reason.
3.3 Namespaces
- Avoid “using-directives” (using namespace some_namespace) so that
the scope of all functions, variables, constants etc. are properly
defined
- Enumerations and constants should be contained in a namespace, if
not already contained in a class or struct
3.4 Exceptions
- Exceptions are the preferred method of handling errors (as opposed
to returning error codes)
- Do not throw exceptions in destructors or callbacks that are not
directly invoked
- If you have code that can be interrupted by exceptions, you must
ensure that all resources are properly deallocated (delete pointers,
release mutexes)
3.5 Print Statements
- Use the ROS console style of printing (e.g. ROS_INFO,
ROS_ERROR_STREAM …)
- This style of printing allows the use of different logger levels,
which is useful in debugging
- See
http://wiki.ros.org/roscpp/Overview/Logging
for details
3.6 Macros
- Avoid defining macros. They are difficult to debug, can lead to
unexpected results in expansion, and also have global scope which
can lead to naming conflicts.
- Or if their usage is restricted to a particular scope just #undef
them afterwards.
3.7 Global and Static Variables
- Use of global and static variables are strongly discouraged
- Global and static variables both prevent multiple instantiations of
a piece of code and will make multi-threaded programming difficult
- Use Object-Oriented Programming techniques and declare variables as
class variables
3.8 Constant Variables
- It is often good practice to explicitly identify constant variables
using const
- This helps prevent unwanted modifications to variables whose values
should never change
- Using const & will also prevent compiler from copying data
unnecessarily
3.9 Assertions
- Use assertions to check preconditions, data structure integrity, and
the return value from a memory allocator
- Usually assertions can be used to detect undefined behaviour
- Use caution to make sure that abrupt exit of system is fail-safe
3.10 Passing/Returning by Reference/Value
Pass simple types by value, not reference - use const when values are
immutable
- Passing/returning by reference leads to pointer operations instead
of passing values in processor registers, which is much slower
Returning by & or const can have significant performance savings when
the normal use of the returned value is for observation
Returning by value is better for thread safety and if the normal use of
the returned value is to make a copy anyhow, there's no performance
lost
If your API uses covariant return types, you must return by & or *
Temporary and local values are always returned by value
More details:
https://github.com/lefticus/cppbestpractices/issues/21
3.11 Memory Leaks and Smart Pointers
- Working with raw memory access, allocation and deallocation in C++
can risk memory errors and leaks
- When allocating memory on the heap (e.g. using new), ALWAYS remember
to delete the object and free the pointer if applicable
- Use smart pointers provided by C++ to alleviate issues in memory
- Avoid shared_ptr since it closely resembles the attributes of a
global variable in terms that it allows multiple pieces of code to
interact with the same data
Good Example:
auto myobj = std::unique_ptr<MyClass>(new MyClass(constructor_param1,
constructor_param2));
auto mybuffer = std::unique_ptr<char[]>(new char[length]);
// or for reference counted objects
auto myobj = std::make_shared<MyClass>();
// ...
// myobj is automatically freed for you whenever it is no longer used.
Difficult to avoid memory errors when:
MyClass *myobj = new MyClass;
// ...
delete myobj;
3.12 Forward Declaration
- Use forward declaration whenever possible to reduce compile time and
minimize header and library dependencies for code that doesn’t need
to know implementation details
- Helps avoid circular dependency issues and avoid including items you
don’t need
Using include:
//file C.h
#include "A.h"
#include "B.h"
class C;
Using forward declarations:
//file C.h
#include "B.h"
class A;
class C;
//file C.cpp
#include "C.h"
#include "A.h"
Document generated by Confluence on Dec 10, 2021 04:02
Atlassian