产品服务 Github 技术交流 激光雷达 ROS教程 深度学习 机器视觉

ROS C++代码风格说明


  • administrators

    1. 动机
      代码风格是重要的。整洁一致的代码风格能够让代码更加容易阅读,更容易测试和维护。我们努力编写程序不仅让它现在能够正常工作,同时也要保证能够在许多年后被其他开发者继续使用。

    为了这个目的,我们规定了一系列规则。我们的目标是鼓励敏捷且合理的能够让其他开发者容易理解的代码。

    这些是指导准则并不是规则。这篇文章并不完全禁止一些C++的模式或者功能,只是介绍更好的实现方式。当偏离这个准则的时候你要在代码中说明原因。

    最重要的是保持一致性。尽可能的遵守此准则。但是如果你是在更改别人的已经存在的软件包。那就要遵循别人已经存在的代码风格。

    1. ROS代码自动格式规范化

    为什么在我们忙着创造机器人的时候要话大量时间去手动修改代码格式。用机器人来自动格式规范化你的代码吧。这些指令能够自动规范你的代码。

    1. 有很多C++的代码是在这个标准发布之前写的。所有在代码库中有很多代码是不符合这个标准的。下面是在使用这些代码是的建议
    • 所有新开发的软件包都应该符合这个标准
    • 除非你有很多空闲时间,否则不要去尝试把代码修改成符合此规则。
    • 如果你是一个软件包的作者,请花时间把软件包更新为符合这个规则。
    • 如果你在小范围的修改一个软件包,请遵循这个软件包自己的代码风格。不要混合不同风格的代码。
    • 如果你在大范围修改一个软件包,那么请花时间把这个软件包修改为符合此代码风格。
    1. 命名规则
      下面的词用来代指各种命名规则
      • CamelCased: 第一个字母大写,以后每个词的首字母大写
      • camelCased: 第一个字母小写,以后每个词的首字母大写
      • under_scored: 只使用小写字母,不同词之间用下划线隔开
      • ALL_CAPITALS: 只使用大写字母,不同的词间用下划线隔开。

    4.1 软件包
    ROS软件包的命名规则为under_scored
    这个不止适用于C++的代码。查看开发指南了解更多信息

    4.2 话题和服务
    ROS话题和服务的名称命名规则为under_scored
    同样这也不止适用于C++代码。查看开发指南了解更多信息

    4.3 文件
    所有的文件名都是 under_scored
    源文件的扩展名为.cpp
    头文件的扩展名为.h
    要描述清楚,比如不要使用laster.cpp使用hokuyo_topurg_laser.cpp.
    如果一个文件主要是实现一个class.那么就根据这个类去命名文件。比如 ActionServer 的文件为action_server.h

    4.3.1 库文件
    库文件命名规则为under_scored
    不要在lib前缀之后直接添加库名称
    比如

    lib_my_great_thing ## Bad
    libmy_great_thing ## Good
    

    4.4 类和类型
    类的命名规则为CamelCased
    比如

    class ExampleClass;
    

    例外情况,如果这个类名字本身包含缩写那么缩写名称要大写,比如

    class HokuyoURGLaser;
    

    根据这个类是什么来命名这个类。如果你无法描述这个类是什么也许你还没有设计好。如果一个类的名称包含了三个词以上,那么这可能是由于你的设计不够清晰。

    请参考 Google: Type names

    4.5 函数和方法
    通常情况下函数和方法的命名规则是camelCased,其参数的命名规则是under_scored,比如

    int exampleMethod(int example_arg);
    

    函数和方法通常是为了实现否个行为,所以他们的名称要体现出它们要做什么。比如使用checkForErrors()而不要使用errorCheck(),使用dumpDataToFile() 而不要使用dataFile()。类通常都是名词。通过把函数名称设计为动词可以让其他的命名更加自然。

    4.6 变量
    通常情况下变量名是under_scored
    变量命名要尽可能的具有描述性。更长的变量名并不会占用更多的内存。当然整型迭代变量可以非常的短。比如i,j,k.但是变量使用一定要保持一致性。比如i在外部循环,j在内部循环。

    STL迭代变量要能看出来他们是迭代器

    std::list<int> pid_list;
    std::list<int>::iterator pid_it;
    

    或者从变量名能够看出迭代器的类型

    std::list<int> pid_list;
    std::list<int>::iterator int_it;
    

    4.6.1 常量
    常量使用ALL_CAPITALS
    4.6.2 成员变量
    类的成员变量使用 under_scored。同时尾部添加一个下划线

    int example_int_;
    

    4.6.3 全局变量
    尽可能避免使用全局变量。当使用时,全局变量要under_scored 同时在前面加上g_

    // I tried everything else, but I really need this global variable
    int g_shutdown;
    

    4.7 命名空间
    命名空间要under_scored

    1. 协议声明
      所有的源文件和头文件头部都必须包含协议和版权声明。
      在ros-pkg和wg-ros-pkg源LICENSE 文件夹中包含了协议的模板。
      查看ROS开发指南了解更多关于协议和版权的信息。

    2. 格式
      你的编辑器应该能够自动设置格式。
      每一个块级结构要缩进两个空格,不要使用tab
      命名空间的内容并不需要缩进。
      大括号无论是左括号还是右括号都要独占一行。

    if(a < b)
    {
      // do stuff
    }
    else
    {
      // do other stuff
    }
    

    如果只有一行代码,那么括号可以省略

    if(a < b)
      x = 2*a;
    

    当执行的代码比较复杂的时候不要省略大括号

    if(a < b)
    {
      for(int i=0; i<10; i++)
        PrintItem(i);
    }
    

    下面是一个更完整的例子

    /*
     * A block comment looks like this...
     */
    #include <math.h>
    class Point
    {
    public:
      Point(double xc, double yc) :
        x_(xc), y_(yc)
      {
      }
      double distance(const Point& other) const;
      int compareX(const Point& other) const;
      double x_;
      double y_;
    };
    double Point::distance(const Point& other) const
    {
      double dx = x_ - other.x_;
      double dy = y_ - other.y_;
      return sqrt(dx * dx + dy * dy);
    }
    int Point::compareX(const Point& other) const
    {
      if (x_ < other.x_)
      {
        return -1;
      }
      else if (x_ > other.x_)
      {
        return 1;
      }
      else
      {
        return 0;
      }
    }
    namespace foo
    {
    int foo(int bar) const
    {
      switch (bar)
      {
        case 0:
          ++bar;
          break;
        case 1:
          --bar;
        default:
        {
          bar += bar;
          break;
        }
      }
    }
    } // end namespace foo
    

    6.1 行的长度
    一行最多包含120个字符

    6.2 #ifndef 保护
    所有的头文件都必须用#ifndef保护起来

    #ifndef PACKAGE_PATH_FILE_H
    #define PACKAGE_PATH_FILE_H
    ...
    #endif
    

    这部分保护代码要立即添加在协议声明之后。同时#endif添加在文件尾部。

    1. 文档
      代码必须要有文档。没有文档的代码也许能够工作但是没有办法维护。
      我们使用doxygen来自动生成文档。Doxygen 会解析你的代码从代码中提取出对应的文档。

    查看rosdoc页面了解更多的doxygen文档信息。

    所有的方法,函数,类,类成员变量,枚举类型,和常量都需要文档说明。

    1. 终端输出
      避免使用printf 和cout。使用rosconsole来输入信息。其具有下面的优点
    • 有颜色
    • 能够通过显示级别来控制输出
    • 通过/rosout发布,所以网络中的其他用户也能查看
    • 可以选择输出到文件中。
    1. 宏定义

    尽量避免使用宏定义。和常量变量和内联函数不同,宏定义既没有类型也没有作用域。

    1. 预处理指令(#if #ifdef)
      对于条件编译的情况,总是使用#if 不要使用#ifdef
      有些人会这样写代码
    #ifdef DEBUG
            temporary_debugger_break();
    #endif
    

    其他人可能会这样编译

    cc -c lurker.cpp -DDEBUG=0
    

    总是使用#if,。这样能够保证程序正常工作。即使DEBUG没有定义。

    #if DEBUG
            temporary_debugger_break();
    #endif
    
    1. 输出参数
      函数和方法中的输出参数通过传指针进行而不是传引用。
    int exampleMethod(FooThing input, BarThing* output);
    

    通过传引用的方式,调用者无法判断参数是否能被修改。

    1. 命名空间
      鼓励使用命名空间来给代码增加作用域。根据软件包的功能选择一个具有描述性的名称。不要在头文件中使用using-directives . 否则所有使用这个头文件的文件的命名空间都会被污染.使用 using-declarations。这样只有想要使用的名称才会被影响。
      比如不要使用
    using namespace std; // Bad, because it imports all names from std::
    

    使用

    using std::list;  // I want to refer to std::list as list
    using std::vector;  // I want to refer to std::vector as vector
    
    1. 继承
      继承是经常用来定义接口的方式。基础类定义了接口,然后子类实现对应接口。
      继承也通常被用来给子类提供通用的代码。这种继承的使用方法是不建议的。因为子类中包含了基类的实例,所以可以实现同样的目的,同时也有更少的迷惑性。

    当在子类中覆盖一个虚拟方法时,一定要声明称virtual,这样读者才知道发生了什么。

    13.1 多重继承
    尽量避免多重继承,它会产生各种各样的问题。

    1. 异常
      异常是相对于返回整型错误码更好的一种错误报告方式。
      一定要把你的软件包中每个函数和方法可能抛出的异常在文档中说明。
      不要在destructors中抛出异常。
      不要在你不直接调用的回调函数中抛出异常
      如果你在代码中使用错误码而不使用异常,那么就一直使用错误码。一定要保持一致性。

    14.1 编写异常安全的代码
    当你的代码能过够被异常中断时,一定要确认所有的资源都被释放。比如互斥锁要释放,动态内存要释放。

    1. 枚举类型
      把你的枚举类型放在命名空间中
    namespace Choices
    {
      enum Choice
      {
         Choice1,
         Choice2,
         Choice3
      };
    }
    typedef Choices::Choice Choice;
    

    这样方式枚举类型污染命名空间。枚举类型中的元素可以Choices::Choice1这样访问。但是typedef 仍然允许在命名空间外使用去声明Choice枚举类型。

    1. Globals
      全局的无论是变量还是方法都要尽量避免使用。他们会污染命名空间,减少代码的重用性。

    全局变量是最最应该避免的。它阻碍了多实例,同时使得多线程编程成为噩梦。

    大部分的变量和函数应当在类里面声明。剩下的要在命名空间中声明。
    例外情况: 一个文件可能包含main()函数和其他一些小的全局的工具函数。但是注意这些有用的函数可能以后也对其他人很有用。

    1. 静态类变量
      尽量避免使用静态类变量。它同样也会使得无法多次实例化代码,同时也使得多线程编程变成一个噩梦

    2. 调用exit()
      只在设计好的一个退出程序的地方调用exit()
      不要在库文件中调用exit

    3. Assertions
      使用assertions 来检查条件,数据结构,和内存管理器的返回值。使用Assertions 要比使用条件声明更好。
      不要直接使用assert()。使用在ros/assert.h里面声明的函数

    /** ROS_ASSERT asserts that the provided expression evaluates to
     * true.  If it is false, program execution will abort, with an informative
     * statement about which assertion failed, in what file.  Use ROS_ASSERT
     * instead of assert() itself.
     * Example usage:
     */
       ROS_ASSERT(x > y);
    
    /** ROS_ASSERT_MSG(cond, "format string", ...) asserts that the provided
     * condition evaluates to true.
     * If it is false, program execution will abort, with an informative
     * statement about which assertion failed, in what file, and it will print out
     * a printf-style message you define.  Example usage:
     */
       ROS_ASSERT_MSG(x > 0, "Uh oh, x went negative.  Value = %d", x);
    
    /** ROS_ASSERT_CMD(cond, function())
     * Runs a function if the condition is false. Usage example:
     */
       ROS_ASSERT_CMD(x > 0, handleError(...));
    
    /** ROS_BREAK aborts program execution, with an informative
     * statement about which assertion failed, in what file. Use ROS_BREAK
     * instead of calling assert(0) or ROS_ASSERT(0). You can step over the assert
     * in a debugger.
     * Example usage:
     */
       ROS_BREADK();
    

    不要在assertion里面运行程序。只用它来检查逻辑条件。根据编译的设置assertion可能不会被执行。

    在软件开发的时候开启assertion检查时很必要的。当软件接近完成,在深入的测试代码中assertion一直成立。那么你可以添加一个编译flag,把assertion代码从编译中移除。这样这部分代码就不会在占用空间和CPU资源。下面的catkin_make选项会为你的所有的软件包定义NDEBUG宏,从而移除assertion检查。

    catkin_make -DCMAKE_CXX_FLAGS:STRING="-DNDEBUG"
    

    注意: 当你执行这个命令后cmak会编译你的所有软件。同时会把设置记录下来。除非你删除build和devel文件夹。