[译] ROS C++ 代码规范
內容說明 : 文章內容翻譯自ROS Wiki,也引用了部分《代碼整潔之道》書中的內容。
 ROS C++代碼規范與谷歌C++代碼規范有諸多相似之處,本文主要講述在編寫ROS C++代碼時需要遵守的編程規范。無論是ROS官方代碼還是用戶自定義代碼,該規范都適用。
 感謝ROS wiki提供的資料,由于譯者個人水平有限,文中難免有錯誤出現。 如有發現,請及時與我聯系,感激不盡!!
文章目錄
- 前言 什么是整潔代碼
- 1. 代碼規范的重要性
- 2. ROS代碼格式自動化工具
- clang_format指南
- 2.1. 設置環境
- 2.2. 運行clang_format
- 1. 命令行運行
- 2. VS Code插件
 
 
 
- 3. 如何對待不符合ROS編程規范的代碼
- 4. 良好的命名
- 5. 許可證聲明(License statements)
- 6. 代碼風格
- 6.1. 編輯器自動格式化
- 6.2. 代碼風格規范
 
- 7. 文檔
- 8. 控制臺輸出
- 9. 宏定義
- 10. 預處理命令(#if與#ifdef)
- 11. 輸出參數
- 12. 命名空間
- 13. 繼承
- 14. 異常處理
- 14.1 編寫拋出異常時安全的代碼
 
- 15 枚舉
- 16. 全局變量
- 17. Static class variables
- 18. 調用exit()
- 19. 斷言
- 20. 測試
- 21. 可移植性
- 22. 棄用 Deprecation
- 注
- [1]. 背景資料:糟糕的代碼與混亂的代價 (節選自《代碼整潔之道》)
 
- 參考資料
- 參考鏈接
- 參考文獻
 
前言 什么是整潔代碼
我喜歡優雅和高效的代碼。代碼邏輯應當直截了當,令缺陷難以隱藏;盡量減少依賴關系,使之便于維護;依據某種分層戰略完善錯誤處理代碼;性能調至最優,省得引誘別人做沒規矩的優化,搞出一堆混亂來。整潔的代碼只做好一件事。
 —— Bjarne Stroustrup,C++語言發明者,《C++程序設計語言》(C++Programming Language)一書作者。
整潔的代碼簡單直接。整潔的代碼如同優美的散文。整潔的代碼從不隱藏設計者的意圖,充滿了干凈利落的抽象和直截了當的控制語句。
 —— Grady Booch,《面向對象分析與設計》(Object Oriented Analysis and Design with Applications)一書作者
整潔的代碼應可由作者之外的開發者閱讀和增補。它應當有單元測試和驗收測試。它使用有意義的命名。它只提供一種而非多種做一件事的途徑。它只有盡量少的依賴關系,而且要明確地定義和提供清晰的、盡量少的API。代碼應通過其字面表達含義,因為不同的語言導致并非所有必需的信息均可通過代碼自身清晰表達。
 —— “老大” Dave Thomas,OTl公司創始人,Eclipse戰略“教父”
1. 代碼規范的重要性
代碼風格很重要。 干凈、一致的代碼風格可以使代碼更容易閱讀、調試和維護。
我們努力編寫優雅的代碼,不僅僅是為了簡單地完成當下功能需求,還為了讓這份代碼持續存在,并在未來很多年內被其他開發人員重復使用、閱讀和改進。幾名工程師合作開發一個項目時,做的最多的一件事情就是"看代碼", 每個人都需要能夠看懂其他人的代碼,這個時候,代碼規范就顯得尤為重要。
為此,我們規定(并禁止)各種做法。我們致力于努力開發敏捷且合理的代碼,其他人也可以很容易地理解這些代碼。(Our goal is to encourage agile but reasoned development of code that can be easily understood by others.)
以下(文檔)內容是參考準則,而非規則。(These are guidelines, not rules.) 除了極少數例外,本文檔并未完全禁止任何特定的C++ 模式或功能(C++ pattern or feature),而是描述了在大多數情況下運用的最佳實踐。偏離此處給出的準則時,請務必仔細考慮您的選擇,并在代碼中記錄您這么做的原因。
最重要的是要保持一致性(consistent)。
- 在獨立開發過程中盡可能遵循本指南。
- 如果您正在修改、編輯其他人編寫的ROS package,請遵循該package中的現有樣式約定。如您正在修改編輯的ROS Package遵循Google代碼編程規范,那么您也要遵守Google代碼編程規范(除非您要對整個程序包進行代碼風格改版以遵循本指南)。
2. ROS代碼格式自動化工具
當我們致力于構建性能出色的機器人時,為什么要浪費您大量的寶貴開發時間來格式化代碼呢?
 這里介紹一款出色的工具—— clang-format,參考鏈接:https://github.com/davetcoleman/roscpp_code_format
在博客撰寫時,clang_format工具已在2020年更新,并支持ROS Melodic系統。
 在"使用方法"內容中,選擇了Linux命令行以及VS Code插件來進行說明。更多其他工具請參考相關readme.md文件
clang_format指南
2.1. 設置環境
2.2. 運行clang_format
1. 命令行運行
在終端中切換到catkin_ws工程根目錄下,運行此命令ros_format,即可快速對代碼格式化。
2. VS Code插件
配置好后,在vscode中編輯代碼,保存代碼(ctrl + s)時編輯器會自動按照腳本規則檢查和修改代碼,使其滿足ROS代碼規范。
附: vscode插件工具推薦
3. 如何對待不符合ROS編程規范的代碼
在ROS代碼規范發布之前,已經有許多ROS C++代碼已經編寫好了。因此,ROS代碼庫中有許多不符合ROS編程規范的代碼。以下建議適合于使用不合規范代碼的開發人員:
4. 良好的命名
以下例子表示ROS的命名體系:
| CamelCased | 大駝峰(匈牙利命名法) | 首字母大寫,其后每個單詞首字母大寫 | 用于表示類名、類型。 | class ExampleClass;(類名) class HokuyoURGLaser;(帶縮寫單詞的類名,縮寫字母URG全大寫) | 
| camelCased | 小駝峰(匈牙利命名法) | 首字母小寫,之后單詞首字母大寫 | 方法、函數名 | int exampleMethod(int example_arg); | 
| under_scored | 小寫+下劃線 | 名稱僅使用小寫字母,單詞之間用下劃線分隔。 | ROS packages名稱 ; Topics名; Services名; 文件名(.cpp、.c、.h); 庫名(注意格式是libxxx_yyy,而不是lib_xxx_yyy) ; 命名空間 | ros_openvino_toolkit (功能包名) action_server.h(文件名) libmy_great_thing(庫名) std::list<int> pid_list; (變量名) int example_int_; (成員變量以下劃線_結尾) int g_shutdown; (全局變量以g_開頭) | 
| ALL_CAPITALS | 全部大寫 | 全部字母大寫,單詞之間用下劃線分隔。 | 常量 | PI | 
| __XXXX | 前置下劃線 | 前置下劃線 (__),在命名中不要使用前置下劃線 | 系統保留 | __builtin_expect (一般開發者不需要修改這方面內容) | 
5. 許可證聲明(License statements)
- 每個源文件和頭文件必須在文件開頭包含許可證和版權聲明。
- 在ros-pkg和wg-ros-pkg存儲庫中,LICENSE目錄包含許可證模板,以注釋形式包含在C / C ++代碼中。
文件開頭加入版權公告,然后是文件內容描述。文件包含以下項:
例:
/** Copyright (c) 2018 Intel Corporation** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/如果你對其他人創建的文件做了重大修改,將你的信息添加到作者信息中,這樣后續開發者有疑問時知道該聯系誰。
6. 代碼風格
6.1. 編輯器自動格式化
6.2. 代碼風格規范
- 每個塊縮進2個空格。切勿插入tabs,設定編輯器將tab轉為空格,UNIX / Linux下無條件使用空格。
- 命名空間的內容不縮進。
- 括號,無論是左右括號,都獨占一列。
例:
if(a < b) {// do stuff } else {// do other stuff }- 每行最長120個字符
- 每個頭文件開頭都應該包含#ifndef,防止重復包含。例:
- 盡量不使用非ASCII字符,使用時必須使用UTF-8格式。
7. 文檔
代碼必須有文檔。 沒有文檔的代碼即使現在可以運行,以后也很難維護。清晰、方便理解的注釋不僅對閱讀代碼的人有好處,對自己也非常有用。開發人員經常會遇到這種情況:過一段時間之后,閱讀自己的代碼都有困難。而編寫描述性的注釋對于自己和團隊都有好處。
我們將在project中使用doxygen工具自動生成文檔。 Doxygen工具將解析您的代碼,從特殊格式的塊注釋中提取分析出文檔(函數、變量、類等旁邊),Doxygen也可用于構建更具描述性的自由格式文檔。
 
 Doxygen的安裝:
有關doxygen樣式的注釋示例,可以參考rosdoc頁面。
 所有函數,方法,類,類變量,枚舉和常量都應記錄在案。
舉個例子:在ros_openvino_toolkit中,param_manager.h中的部分代碼:
/*** @brief Parse the give YAML file and generate parameters in ParamManager* instance* @param[in] path The absolute path of the YAML file which is to be parsed.* @return None.*/void parse(std::string path);這些注釋包含了一些奇怪的內容,如“@return”、“@brief”、“@param”等, 這些并不是 C 語言的注釋要求,而是Doxygen注釋風格,使用其同名的軟件,可以根據這種注釋風格的代碼自動生成 API 文檔。
例如,在ros_openvino_toolkit dev-2020.3版本中,dynamic_vino_lib文件夾下已經有Doxyfile文件,此時在此文件夾下運行以下命令:
doxygen ./Doxyfile之后在工程中發現其生成了html和latex兩個文件夾。分別對應latex和html風格的文檔,讀者可依據自己的喜好進行選擇。
 
 譯者在此以html文件夾為例,進入文件夾,打開index.html,即可查看整個工程的文檔
 
 此時查看對象結構也是一目了然:
 
更多關于Doxygen使用說明,推薦一個公開的pdf文檔: Doxygen Quick Reference, 讀者可以自行參閱其中內容,筆者認為這個文檔寫的比較規范與全面。
更多關于Doxygen入門,可以參閱Doxygen官網教程: Getting started
實際上,無論是 pdf 還是網頁版的 API 參考手冊,它們的說明來源都是程序注釋,所以在更多的時候,我們都是直接查看工程中源代碼注釋來了解如何使用。
8. 控制臺輸出
避免使用C或者C++語言風格的字符串輸出(比如printf, cout…)
 可以使用rosconsole來滿足您所有的輸出需求,它提供了帶有printf和stream-style的宏參數。不過其與printf不同的地方是:
9. 宏定義
盡可能避免使用宏。與內聯函數和const變量不同,宏既沒有類型也沒有范圍。
推薦參閱谷歌cpp代碼規范中對于宏的描述
10. 預處理命令(#if與#ifdef)
對于條件編譯(上面6.2小節解釋的#ifndef頭文件保護除外),請始終使用#if,而不是#ifdef。
 有人可能會編寫如下代碼:
其他人可能會在關閉調試信息的情況下來編譯代碼,例如:
cc -c lurker.cpp -DDEBUG = 0這時候就有風險。
如果必須使用預處理器,請始終使用#if。即使根本沒有定義DEBUG,它也可以正常工作,并且做正確的事情。
#if DEBUGtemporary_debugger_break(); #endif11. 輸出參數
方法/函數的輸出參數(例如:函數可以修改的變量),是通過指針而不是通過引用傳遞的。
 例如:
相比之下,當通過引用傳遞輸出參數時,調用者(或后續維護人員)被告知參數是否可以在不讀取方法原型的情況下被修改
推薦參閱Reference Arguments
12. 命名空間
推薦使用namespace來限定代碼范圍,根據package的名稱來選擇一個描述性強的名稱
切勿在頭文件中使用using。這樣做會污染包括頭文件的所有代碼的namespace。
在源文件(cpp)中使用using指令是可以接受的。但是最好使用using-declarations,它僅提取您打算使用的內容。
 例如:
可以改為:
using std::list; // I want to refer to std::list as list using std::vector; // I want to refer to std::vector as vector13. 繼承
使用組合通常比使用繼承更適宜(這一點在GOF在《Design Patterns》里是反復強調的)。如果使用繼承的話,只是用公共繼承。
 當子類繼承父類時,子類包含了父基類所有數據以及操作的定義。
 在C++實踐中,繼承主要用于兩種場合: 實現繼承和接口繼承。
- 實現繼承 (implementation inheitance),子類繼承父類的實現代碼。
- 接口繼承(interface inheritance),子類僅繼承父類的方法名稱。
繼承是定義和實現公共接口的合適手段。基類定義接口,子類實現該接口。(Inheritance is the appropriate way to define and implement a common interface. The base class defines the interface, and the subclasses implement it.)
繼承還可以用于提供從基類到子類的通用代碼。這種情況下不鼓勵使用繼承。(Inheritance can also be used to provide common code from a base class to subclasses. This use of inheritance is discouraged. )
在大多數情況下,“子類”可以包含“基類”的實例,并以較少的混淆可能性實現相同的結果。(discouraged. In most cases, the “subclass” could instead contain an instance of the “base class” and achieve the same result with less potential for confusion.)
子類重載虛擬(virtual)方法時,始終將其聲明為virtual方法,以便讀者了解正在發生的事情。(When overriding a virtual method in a subclass, always declare it to be virtual, so that the reader knows what’s going on.)
強烈建議不要多重繼承,多重繼承允許子類擁有多個父類,它會引起無法容忍的混亂。
參考
14. 異常處理
與返回整數error codes相反,異常(Exceptions)是首選的錯誤報告機制。在測試框架中,異常確實十分好用。
 對于現有代碼,引入異常會牽連到所有依賴代碼,異常會導致程序控制流無法通過查看代碼確定——函數有可能在不確定的地方返回。所以有以下需要注意的地方:
- 始終在每個相關函數/方法上,記錄您的package可能會拋出哪些異常。
- 不要拋出析構函數的異常。
- 不要從您不直接調用的回調中引發異常。
- 如果您在package中選擇使用錯誤代碼代替異常,則僅使用錯誤代碼。 始終如一。
14.1 編寫拋出異常時安全的代碼
當您的代碼可以被異常中斷時,您必須確保當堆棧溢出時,相關資源將被釋放。特別是,必須釋放互斥鎖,并且必須釋放堆分配的內存。
更多內容可以參考StackOverflow : do you really write exception safe code?
15 枚舉
命名您的枚舉,例如
namespace Choices {enum Choice{Choice1,Choice2,Choice3}; } typedef Choices::Choice Choice;這樣可以防止枚舉污染它們所在的命名空間。
 枚舉中的單獨的item引用:Choices :: Choice1。
 typedef仍然允許聲明Choice enum而不是命名空間。
如果您使用的是C ++ 11和更高版本,則可以使用范圍枚舉。例如
enum class Choise {Choice1,Choice2,Choice3 }; Choise c = Choise::Choice1;Enumerator Names
16. 全局變量
不建議使用全局變量(無論變量還是函數)。它們會污染namespace,并使代碼的可重用性降低,耦合性大大提高,使得維護變得困難。它們阻止代碼的多個實例化,并使多線程編程成為一場噩夢。(They prevent multiple instantiations of a piece of code and make multi-threaded programming a nightmare.)
大多數變量和函數應在類內部聲明。其余應在namespace中聲明。
例外:文件可能包含main()函數和一些全局的小輔助函數。但是請記住,有一天這些輔助功能可能對其他人有用。
參考閱讀
- Google:Static and Global Variables
- Google:Nonmember, Static Member, and Global Functions
17. Static class variables
不建議使用靜態類變量。它們阻止代碼的多個實例化,并使多線程編程成為一場噩夢。
18. 調用exit()
僅在應用程序中定義明確的退出點(exit point)時調用exit()。
 切勿在庫中調用exit()。
19. 斷言
使用斷言檢查先決條件,數據結構完整性和內存分配器的返回值。
 斷言比編寫條件語句要好,后者很少會被執行。
不要直接調用assert()。而是使用在ros / assert.h中聲明的以下函數之一(rosconsole軟件包的一部分):
- ROS_ASSERT(x > y);
- ROS_ASSERT_MSG(x > 0, “Uh oh, x went negative. Value = %d”, x);
- ROS_ASSERT_CMD(x > 0, handleError(…));
- ROS_BREADK();
不要在斷言中做任何工作;僅檢查邏輯表達式。取決于編譯環境的設置,可能不會執行該斷言。
 通常會開發啟用了斷言檢查的軟件,以捕獲異常情況(in order to catch violations)。
 當軟件即將完成時,并且在進行大量測試時發現斷言始終是正確的時,您將使用一個標志從編譯中刪除斷言,從而使它們不占用任何空間或時間。
 catkin_make的以下選項將為所有ROS package定義NDEBUG宏,從而刪除斷言檢查。
注意:當您使用此命令運行cmake時,它將重新全部編譯,并且在后續運行catkin_make時會記住相關設置,直到刪除build和devel目錄重新編譯為止。
20. 測試
參考閱讀: GTEST
21. 可移植性
保持C ++代碼的可移植性很重要。以下是注意事項:
22. 棄用 Deprecation
注
[1]. 背景資料:糟糕的代碼與混亂的代價 (節選自《代碼整潔之道》)
只要你干過兩三年編程,就有可能曾被某人的糟糕的代碼絆倒過。如果你編程不止兩三年,也有可能被這種代碼拖過后腿,進度延緩的情況會很嚴重。有些團隊在項目初期進展迅速,但有那么一兩年的時間卻慢如蝸行。對代碼的每次修改都影響到其他兩三處代碼,修改無小事。每次添加或修改代碼,都得對那堆扭紋柴了然于心,這樣才能往上扔更多的扭紋柴。這團亂麻越來越大,再也無法理清,最后束手無策。
 隨著混亂的增加,團隊生產力也持續下降,以致趨向于零。當生產力下降時,管理層就只有一件事可做了:增加更多人手到項目中,期望提升生產力。可是新人并不熟悉系統的設計。他們搞不清楚什么樣的修改符合設計意圖,什么樣的修改違背設計意圖。而且,他們以及團隊中的其他人都背負著提升生產力的可怕壓力。于是,他們只會制造更多的混亂,驅動生產力向零那端不斷下降。
 
參考資料
參考鏈接
- 英文版: https://google.github.io/styleguide/cppguide.html#Background
- 中文版: https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/
https://blog.csdn.net/softimite_zifeng/article/details/78357898
參考文獻
總結
以上是生活随笔為你收集整理的[译] ROS C++ 代码规范的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: ros_openvino_toolkit
- 下一篇: 大于2的质数判断以及范围质数查找
