Code Conventions
To improve readability and simplify maintenance we developed a set of conventions to apply to when writing code for Finroc or RRLib. These conventions are no a matter of personal taste but of consistency and a uniform appearance makes it easier for everyone to integrate new developers and their code. Unfortunately, some of the older parts have not been ported completely to this standard, yet.
However, the rules below are not guidelines or recommendations, but strict rules. New contributions will not be accepted if they do not apply to these rules.
Naming
This sections describes how to name things. A few general rules apply to all names:
- Every name must be an english expression. Don't use other languages or even mix them.
- Do not abbreviate words. This is often unnecessary as most editors have the possibility of auto-completion and the code is less readable.
Classes and filenames
Use the commandline tool finroc_create
to generate new files for new classes. It will take care of correct naming, formatting and file creation.
Variables / Fields / Values
Their names may consist of phrases, whereas words are separated by underscores. We distinguish between global constant values and other variables.
General case
The name is all lowercase.
int an_integer_variable;
float distance_to_obstacle;
bool activated;
std::vector<double> list_of_numbers;
const std::vector<double> &const_reference_to_number_list(list_of_values);
Global constant values
The name is all uppercase and has the prefix c
.
Functions / Methods
We use camelback for their names. Words are connected without underscore and start with a capital letter each. Regarding concerns the name of a function should precisely describe what it does. Think of using natural language. In most cases using a verbial expression if the method or function changes its object or argument (like an order) and an adverbial expression or adjective if you want to get a property or value makes it easy to understand what the function really does.
The parameters of course must conform to the coding standards for variables.
bool IsActivated();
bool Initialized();
void Initialize();
void Rotate(double angle);
float DistanceToObstacle(const tObstacle &obstacle);
Type declarations
Type and classes follow the same scheme. Their names are formatted in camelback and have a prefix that distinguishes between
- parts:
p
- groups:
g
- modules
m
- all the rest
t
typedef const char *tDescription;
typedef unsigned char tThisIsAByteDeclaration;
typedef core::mGenericModule<double> mGenericModule;
Note that when talking about camelback case formatting abbreviations are not words. Therefore tTcpConnection
is not correct and must be tTCPConnection
.
Type-Traits and Policy-Classes
In C++ template programming special template classes are used to make decisions at compile-time like switch-case constructions or implementing the strategy pattern. Their names follow the same scheme like normal classes but as they do not declare types of objects in our runtime code, the do not have a prefix at all.
Enumerations
Enumeration values are like global constant variables all uppercase with underscores but have a different prefix. Old-style (C99) enums need at least the prefix e
to be easily identified as such. If the enum is not anonymous the prefix should also include the typename, all uppercase and with underscores. Strongly typed C++11 enums do not need a prefix at all, as their typename must be prepended.
It is always possible to append the special value DIMENSION
as endmarker for looping or invalid default values.
enum tParameter
{
ePARAMETER_BOOL,
ePARAMETER_INT,
ePARAMETER_FLOAT,
ePARAMETER_SRING,
ePARAMETER_DIMENSION
};
enum tEventType
{
eEVENT_TYPE_PARAMETER_CHANGED,
eEVENT_TYPE_PREPARE_FOR_FIRST_SENSE,
eEVENT_TYPE_PREPARE_FOR_FIRST_CONTROL,
eEVENT_TYPE_PREPARE_FOR_BREAK,
eEVENT_TYPE_PREPARE_FOR_RESTART,
eEVENT_TYPE_DIMENSION
};
enum
{
eERROR,
eSTART,
eSTOP
};
enum class tLogLevel
{
ERROR,
WARNING,
DEBUG
}
The name of an enumeration itself is a typename and therefore must be prefixed with a t
.
Templates
C++ templates are generic definitions that describe code that would have been written for a specific case. Thus, the same rules as for other code apply to the code to be generated.
However, some additional conventions come along with templates:
Specifier and parameter list
The specifier template
and the parameter list always go into a separate line.
Class and function names
Use the same names like for non-template classes or functions. Templates are classified as such in their declaration and never need a suffix T
or whatever.
Parameters
Template parameters can be compile-time constant integral types like bool
, int
, size_t
, etc. For these, the usual guidelines for variable names apply except that they have a prefix T
to mark them as being template parameters and as such const for runtime usage.
Furthermore, template parameters can be types. Typically, if the template has only one parameter, its name is T
. However, in analogy to all other naming guidelines the name of the parameter should be correlated with its role within code generation. At least, if the template has more than one parameter, all types have to be named like classes with a leading T
.
template <int Tdimension, typename TElement>
class tVector;
template <int Tdimension, typename TElement, template <int, typename> class TData>
class tAdvancedVector;
template <typename TLeft, typename TRight>
void DoSomeOperation(const TLeft &left, const TRight &right);
Layout and formatting
File/Class layout and formatting rules help to give the code in Finroc and RRLib a uniform appearance. Especially formatting is the only way to create meaningful diffs in our code revision system. Therefore, some the rules are checked via pre-commit-hooks and code that does not comply can not be added to the repositories.
General formatting rules
- Do not use tabs, only spaces.
- Indentation is two spaces for each level
- Pad operators
+
,-
,*
,/
,%
- Do not pad operators
()
,[]
,.
,->
- Block markers
{
and}
always go on their own lines, except for the empty block{}
- Pad clauses
- Put a space behind
,
- Put
{
and}
always around the body of loops or conditions, even if it is only one statement
void ExampleFunction()
{
int a = 1 + 2;
std::vector<int> numbers({ 1, 2, 3, 4});
for (size_t i = 0; i < numbers.size(); ++i)
{
a + numbers[i];
}
for (auto it = numbers.begin(); it != number.end(); ++it)
{
if (*it == 3)
{
*it = 0;
}
}
}
Class layout
Declare methods and fields in three blocks: public
, protected
and private
, without mixing them. Do not declare variables other than private. Better define inline functions in order to access them. That also gives you control of read and write access to your fields.
Remember: class
has private access level by default whereas struct
defaults to public access.
class tNewClass : public tBaseClass
{
typedef float tOptionalTypedefForInternalUse;
public:
int an_ugly_public_variable;
tNewClass();
virtual ~tNewClass();
inline const int &AFieldOfThisClass() const
{
return a_field_of_this_class;
}
inline int &AFieldOfThisClass()
{
return a_field_of_this_class;
}
inline int AReadOnlyFieldOfThisClass() const
{
return a_read_only_field_of_this_class;
}
protected:
int an_ugly_protected_variable;
int CallMe();
int CallMeLater();
private:
float a_field_of_this_class;
float a_read_only_field_of_this_class;
float an_internal_variable;
int ForInternalUseOnly();
};
Comments
Just like names, all comments written in Finroc and RRLib code must be written in English. Please adhere strictly to this rule as it is very difficult for international programmers to work on code that is commented in other languages.
Keep some thoughts in mind:
Do not comment out invalid code!
We use a distributed source code management system. That does not forget code, so when you think that some code should not be compiled just remove it. If you are wrong and the code is still needed it can be restored.
Piling up out-commented code has the problem that the file will get hard to read and other people will never know if that code is still a valid option or two years old and does not fit to its surrounding at all.
Do not use comments to explain your code!
Comments tend to lie after some time. A whole block about the great underlying ideas of the next 10 lines of code can get totally wrong an misleading if the next programmer finds a bug in these ideas and changes the incorrect code. There is no compiler that checks if he also updated the comment.
Better make your code self-explanatory. By choosing good variable names and function names and applying good separation of concerns, you should be able to explain your code within itself.
Use Doxygen for you API
Of course, according to the last point the API of your code should contain self-explanatory names. The problem of aging and hence lying comments exists here, too. But we also want to create a proper API documentation using Doxygen. So please add suitable comments with Doxygen markers to you header files.
//! Short description of tImageLoader (that is also used in Doxygen)
/*! A more detailed description of tImageLoader, which
spans two lines and says nothing. (But is used in Doxygen)
*/
class tImageLoader
{
public:
/*! The ctor of tImageLoader
*
* Some more explanation on this method.
*
* \param num_streams The number of streams to use
* \param delay How many milliseconds to wait before loading the next image
*/
tImageLoader(unsigned int num_streams, float delay);
};
Use FIXME and TODO
Sometimes written code can not be in its final form. Maybe we know that a nice C++11 feature will allow to rephrase a line but is not supported by the compiler, yet. Or we wait for a feature that another programmer will add to a library we are using.
Mark these spots with comments containing the keywords FIXME or TODO, but also use them. That means: In regular intervals search for your TODOs and FIXMEs and do or fix them.
Good programming behavior
While the previous rules helped to create better looking code, the following ones also help to create better working code.
Accessing members (variables and methods)
It is a very good idea to choose different names for instance variables (variables declared as members of an object) and other, temporary variables. This helps the reader to find out whether some expression uses a temporary variable (like a function parameter) or some 'global variable' declared in the class definition. It also helps you to avoid problems that can arise when a temporary variable shadows some instance variable with the same name (see the following example).
class tNewClass
{
public:
void MyNewMethod(int value);
private:
int last_value;
};
tNewClass::MyNewMethod(int value)
{
int last_value;
.
.
. some code goes here
.
.
// try to save the value in the instance variable
last_value = value;
// ERROR! The temporary variable 'last_value' is used instead of the instance variable. Data will be lost when the method ends.
}
The best way to avoid this naming conflict is to access members (variables and methods) using the this
pointer. In addition to solving the shadowing problem shown above, this convention documents the origin of a variable or method for the reader and solves binding problems concerning template programming which strange conventions like adding underscores do not.
class tNewClass
{
public:
void MyNewMethod(int value);
private:
int last_value;
};
tNewClass::MyNewMethod(int value)
{
int last_value;
.
.
. some code goes here
.
.
// this time really save the value in the instance variable
this->last_value = value;
}
In any case, we strongly recommend that you always use this
to access member variables and methods. That way you do it automatically right and again: supports consistent and uniform appearance.
Initializing instance variables in the class constructor
C++ offers a special way to initialize instance variables in the class constructor. This allows for better inheritance and the declaration of const variables without directly specifying their value upon declaration. It also avoids implicit default construction and is needed if no default ctor is present. Therefore, use the constructor's init block.
class tNewClass
{
public:
tNewClass();
tNewClass(const std::string &name);
private:
float instance_variable_one;
int instance_variable_two;
std::string my_name;
};
tNewClass::tNewClass() :
instance_variable_one(1.4),
instance_variable_two(3),
my_name("default_name")
{
std::cout << "tNewClass constructed." << std::endl;
}
tNewClass::tNewClass(const std::string &name) :
instance_variable_one(1.4),
instance_variable_two(3),
my_name(name)
{
std::cout << "tNewClass constructed." << std::endl;
}
#define
The #define directive belongs to the ancient C preprocessor macros. It may only be used to compile code depending on defined switches, not for constants or 'optimized functions'. In order to define constant values we recommend to use constant variables of a specific type. For small helper functions use inline functions. This allows the compiler to do type checking and generate warnings or even errors. It also allows to limit the scope of the const variables and helper functions.
Console output and logging
Finally some thoughts on writing text to the console.
Programms created in Finroc are not meant to talk to the user via the terminal they might be started in. They control a robot and typically have a graphical user interface connected that can be used for interaction. Finroc and RRLib use a their own logging facility rrlib_logging
that allows better tracking where log messages come from, filtering via log levels and domains and optimization for releases without debug information.
Therefore do not use std::cout or printf to write text to stdout or stderr. Use RRLIB_LOG_PRINT
or RRLIB_LOG_PRINTF
or in Finroc FINROC_LOG_PRINT
or FINROC_LOG_PRINTF
to create output with streaming or printf semantic.