当前位置:主页   - 电脑 - 程序设计 - C/C++
C++ 工程实践(3):采用有利于版本管理的代码格式
来源:网络   作者:   更新时间:2012-02-10
收藏此页】    【字号    】    【打印】    【关闭

版本管理(version controlling)是每个程序员的基本技能,C++ 程序员也不例外?版本管理的基本功能之一是追踪代码变化,让你能清楚地知道代码是如何一步步变成现在的这个样子,以及每次 check-in 都具体改动了哪些内部?无论是传统的集中式版本管理工具,如 Subversion,还是新型的分布式管理工具,如 Git/Hg,比较两个版本(revision)的差异都是其基本功能,即俗称“做一下 diff”?

diff 的输出是个窥孔(peephole),它的上下文有限(diff –u 默认显示前后 3 行)?在做 code review 的时候,如果能凭这“一孔之见”就能发现代码改动有问题,那就再好也不过了?

C 和 C++ 都是自由格式的语言,代码中的换行符被当做 white space 来对待?(当然,我们说的是预处理(preprocess)之后的情况)?对编译器来说一模一样的代码可以有多种写法,比如

foo(1, 2, 3, 4);

foo(1,    2,    3,    4);

词法分析的结果是一样的,语意也完全一样?

对人来说,这两种写法读起来不一样,对与版本管理工具来说,同样功能的修改造成的差异(diff)也往往不一样?所谓“有利于版本管理”,就是指在代码中合理使用换行符,对 diff 工具友好,让 diff 的结果清晰明了地表达代码的改动?(diff 一般以行为单位,也可以以单词为单位,本文只考虑最常见的 diff by lines?)

这里举一些例子?

对 diff 友好的代码格式

1. 多行注释也用 //,不用 /* */

Scott Meyers 写的《Effective C++》第二版第 4 条建议使用 C++ 风格,我这里为他补充一条理由:对 diff 友好?比如,我要注释一大段代码(其实这不是个好的做法,但是在实践中有时会遇到),如果用 /* */,那么得到的 diff 是:

diff --git a/examples/asio/tutorial/timer5/timer.cc b/examples/asio/tutorial/timer5/timer.cc--- a/examples/asio/tutorial/timer5/timer.cc+++ b/examples/asio/tutorial/timer5/timer.cc@@ -18,6 +18,7 @@ class Printer : boost::noncopyable     loop2_->runAfter(1, boost::bind(&Printer::print2, this));   }+  /*   ~Printer()   {     std::cout << "Final count is " << count_ << "\n";@@ -38,6 +39,7 @@ class Printer : boost::noncopyable       loop1_->quit();     }   }+  */   void print2()   {

从这样的 diff output 能看出注释了哪些代码吗?

如果用 //,结果会清晰很多:

diff --git a/examples/asio/tutorial/timer5/timer.cc b/examples/asio/tutorial/timer5/timer.cc--- a/examples/asio/tutorial/timer5/timer.cc+++ b/examples/asio/tutorial/timer5/timer.cc@@ -18,26 +18,26 @@ class Printer : boost::noncopyable     loop2_->runAfter(1, boost::bind(&Printer::print2, this));   }-  ~Printer()-  {-    std::cout << "Final count is " << count_ << "\n";-  }+  // ~Printer()+  // {+  //   std::cout << "Final count is " << count_ << "\n";+  // }-  void print1()-  {-    muduo::MutexLockGuard lock(mutex_);-    if (count_ < 10)-    {-      std::cout << "Timer 1: " << count_ << "\n";-      ++count_;--      loop1_->runAfter(1, boost::bind(&Printer::print1, this));-    }-    else-    {-      loop1_->quit();-    }-  }+  // void print1()+  // {+  //   muduo::MutexLockGuard lock(mutex_);+  //   if (count_ < 10)+  //   {+  //     std::cout << "Timer 1: " << count_ << "\n";+  //     ++count_;+  //+  //     loop1_->runAfter(1, boost::bind(&Printer::print1, this));+  //   }+  //   else+  //   {+  //     loop1_->quit();+  //   }+  // }   void print2()   {

同样的道理,取消注释的时候 // 也比 /* */ 更清晰?

另外,如果用 /* */ 来做多行注释,从 diff 不一定能看出来你是在修改代码还是修改注释?比如以下 diff 似乎修改了 muduo::EventLoop::runAfter 的调用参数:

diff --git a/examples/asio/tutorial/timer5/timer.cc b/examples/asio/tutorial/timer5/timer.cc--- a/examples/asio/tutorial/timer5/timer.cc+++ b/examples/asio/tutorial/timer5/timer.cc@@ -32,7 +32,7 @@ class Printer : boost::noncopyable       std::cout << "Timer 1: " << count_ << "\n";       ++count_;-      loop1_->runAfter(1, boost::bind(&Printer::print1, this));+      loop1_->runAfter(2, boost::bind(&Printer::print1, this));     }     else     {

其实这个修改发生在注释里边 (要增加上下文才能看到, diff -U 20,多一道手续,降低了工作效率),对代码行为没有影响:

diff --git a/examples/asio/tutorial/timer5/timer.cc b/examples/asio/tutorial/timer5/timer.cc--- a/examples/asio/tutorial/timer5/timer.cc+++ b/examples/asio/tutorial/timer5/timer.cc@@ -20,31 +20,31 @@ class Printer : boost::noncopyable   /*   ~Printer()   {     std::cout << "Final count is " << count_ << "\n";   }   void print1()   {     muduo::MutexLockGuard lock(mutex_);     if (count_ < 10)     {       std::cout << "Timer 1: " << count_ << "\n";       ++count_;-      loop1_->runAfter(1, boost::bind(&Printer::print1, this));+      loop1_->runAfter(2, boost::bind(&Printer::print1, this));     }     else     {       loop1_->quit();     }   }   */   void print2()   {     muduo::MutexLockGuard lock(mutex_);     if (count_ < 10)     {       std::cout << "Timer 2: " << count_ << "\n";       ++count_;

总之,不要用 /* */ 来注释多行代码?

或许是时过境迁,大家都在用 // 注释了,《Effective C++》第三版去掉了这一条建议?

2. 局部变量与成员变量的定义

基本原则是,一行代码只定义一个变量,比如

double x;double y;

将来代码增加一个 double z 的时候,diff 输出一眼就能看出改了什么:

@@ -63,6 +63,7 @@ private:   int count_;   double x;   double y;+  double z; }; int main()

如果把 x 和 y 写在一行,diff 的输出就得多看几眼才知道?

@@ -61,7 +61,7 @@ private:   muduo::net::EventLoop* loop1_;   muduo::net::EventLoop* loop2_;   int count_;-  double x, y;+  double x, y, z; }; int main()

所以,一行只定义一个变量更利于版本管理?同样的道理适用于 enum 成员的定义,数组的初始化列表等等?

3. 函数声明中的参数

如果函数的参数大于 3 个,那么在逗号后面换行,这样每个参数占一行,便于 diff?以 muduo::net::TcpClient 为例:

class TcpClient : boost::noncopyable{ public:  TcpClient(EventLoop* loop,            const InetAddress& serverAddr,            const string& name);

如果将来 TcpClient 的构造函数增加或修改一个参数,那么很容易从 diff 看出来?这恐怕比在一行长代码里数逗号要高效一些?

4. 函数调用时的参数

在函数调用的时候,如果参数大于 3 个,那么把实参分行写?以 muduo::net::EPollPoller 为例:

Timestamp EPollPoller::poll(int timeoutMs, ChannelList* activeChannels){  int numEvents = ::epoll_wait(epollfd_,                               &*events_.begin(),                               static_cast<int>(events_.size()),                               timeoutMs);  Timestamp now(Timestamp::now());

这样一来,如果将来重构引入了一个新参数(好吧,epoll_wait 不会有这个问题),那么函数定义和函数调用的地方的 diff 具有相同的形式(比方说都是在倒数第二行加了一行内容),很容易肉眼验证有没有错位?如果参数写在一行里边,就得睁大眼睛数逗号了?

5. class 初始化列表的写法

同样的道理,class 初始化列表(initializer list)也遵循一行一个的原则,这样将来如果加入新的成员变量,那么两处(class 定义和 ctor 定义)的 diff 具有相同的形式,让错误无所遁形?以 muduo::net::Buffer 为例:

class Buffer : public muduo::copyable{ public:  static const size_t kCheapPrepend = 8;  static const size_t kInitialSize = 1024;  Buffer()    : buffer_(kCheapPrepend + kInitialSize),      readerIndex_(kCheapPrepend),      writerIndex_(kCheapPrepend)  {  }  // 省略 private:   std::vector<char> buffer_;   size_t readerIndex_;   size_t writerIndex_;   static const char kCRLF[];};

注意,初始化列表的顺序必须和数据成员声明的顺序相同?

6. 与 namespace 有关的缩进

Google 的 C++ 编程规范明确指出,namespace 不增加缩进?这么做非常有道理,方便 diff –p 把函数名显示在每个 diff chunk 的头上?

如果对函数实现做 diff,chunk name 是函数名,让人一眼就能看出改的是哪个函数?如下图,红色划线部分?

查看原图(大图)

如果对 class 做 diff,那么 chunk name 就是 class name?

diff 原本是为 C 语言设计的,C 语言没有 namespace 缩进一说,所以它默认会找到“顶格写”的函数作为一个 diff chunk 的名字,如果函数名前面有空格,它就不认得了?muduo 的代码都遵循这一规则,例如:

namespace muduo{////// Time stamp in UTC, in microseconds resolution.////// This class is immutable./// It's recommended to pass it by value, since it's passed in register on x64.///class Timestamp : public muduo::copyable,                  public boost::less_than_comparable<Timestamp>{// class 从第一列开始写,不缩进// 函数的实现也从第一列开始写,不缩进?Timestamp Timestamp::now(){  struct timeval tv;  gettimeofday(&tv, NULL);  int64_t seconds = tv.tv_sec;  return Timestamp(seconds * kMicroSecondsPerSecond + tv.tv_usec);}

相反,boost 中的某些库的代码是按 namespace 来缩进的,这样的话看 diff 往往不知道改动的是哪个 class 的哪个成员函数?

这个或许可以通过设置 diff 取函数名的正则表达式来解决,但是如果我们写代码的时候就注意把函数“顶格写”,那么就不用去动 diff 的默认设置了?另外,正则表达式不能完全匹配函数名,因为函数名是上下文无关语法(context-free syntax),你没办法写一个正则语法去匹配上下文无关语法?我总能写出某种函数声明,让你的正则表达式失效(想想函数的返回类型,它可能是一个非常复杂的东西,更别说参数了)?更何况 C++ 的语法是上下文相关的,比如你猜 Foo<Bar> qux; 是个表达式还是变量定义?

7. public 与 private

我认为这是 C++ 语法的一个缺陷,如果我把一个成员函数从 public 区移到 private 区,那么从 diff 上看不出来我干了什么,例如:

diff --git a/muduo/net/TcpClient.h b/muduo/net/TcpClient.h--- a/muduo/net/TcpClient.h+++ b/muduo/net/TcpClient.h@@ -37,7 +37,6 @@ class TcpClient : boost::noncopyable   void connect();   void disconnect();-  bool retry() const;   void enableRetry() { retry_ = true; }   /// Set connection callback.@@ -60,6 +59,7 @@ class TcpClient : boost::noncopyable   void newConnection(int sockfd);   /// Not thread safe, but in loop   void removeConnection(const TcpConnectionPtr& conn);+  bool retry() const;   EventLoop* loop_;   boost::scoped_ptr<Connector> connector_; // avoid revealing Connector

从上面的 diff 能看出我把 retry() 变成 private 了吗?对此我也没有好的解决办法,总不能每个函数前面都写上 public: 或 private: 吧?

其它资源
来源声明

版权与免责声明
1、本站所发布的文章仅供技术交流参考,本站不主张将其做为决策的依据,浏览者可自愿选择采信与否,本站不对因采信这些信息所产生的任何问题负责。
2、本站部分文章来源于网络,其版权为原权利人所有。由于来源之故,有的文章未能获得作者姓名,署“未知”或“佚名”。对于这些文章,有知悉作者姓名的请告知本站,以便及时署名。如果作者要求删除,我们将予以删除。除此之外本站不再承担其它责任。
3、本站部分文章来源于本站原创,本站拥有所有权利。
4、如对本站发布的信息有异议,请联系我们,经本站确认后,将在三个工作日内做出修改或删除处理。
请参阅权责声明