陳碩 (giantchen_AT_gmail)
Blog.csdn.net/Solstice
版本管理(version controlling)是每個程序員的基本技能,C++ 程序員也不例外。版本管理的基本功能之一是追蹤代碼變化,讓你能清楚地知道代碼是如何一步步變成現(xiàn)在的這個樣子,以及每次 check-in 都具體改動了哪些內(nèi)部。無論是傳統(tǒng)的集中式版本管理工具,如 Subversion,還是新型的分布式管理工具,如 Git/Hg,比較兩個版本(revision)的差異都是其基本功能,即俗稱“做一下 diff”。
diff 的輸出是個窺孔(peephole),它的上下文有限(diff –u 默認(rèn)顯示前后 3 行)。在做 code review 的時候,如果能憑這“一孔之見”就能發(fā)現(xiàn)代碼改動有問題,那就再好也不過了。
C 和 C++ 都是自由格式的語言,代碼中的換行符被當(dāng)做 white space 來對待。(當(dāng)然,我們說的是預(yù)處理(preprocess)之后的情況)。對編譯器來說一模一樣的代碼可以有多種寫法,比如
foo(1, 2, 3, 4);
和
foo(1,
2,
3,
4);
詞法分析的結(jié)果是一樣的,語意也完全一樣。
對人來說,這兩種寫法讀起來不一樣,對與版本管理工具來說,同樣功能的修改造成的差異(diff)也往往不一樣。所謂“有利于版本管理”,就是指在代碼中合理使用換行符,對 diff 工具友好,讓 diff 的結(jié)果清晰明了地表達(dá)代碼的改動。(diff 一般以行為單位,也可以以單詞為單位,本文只考慮最常見的 diff by lines。)
這里舉一些例子。
對 diff 友好的代碼格式
1. 多行注釋也用 //,不用 /* */
Scott Meyers 寫的《Effective C++》第二版第 4 條建議使用 C++ 風(fēng)格,我這里為他補(bǔ)充一條理由:對 diff 友好。比如,我要注釋一大段代碼(其實(shí)這不是個好的做法,但是在實(shí)踐中有時會遇到),如果用 /* */,那么得到的 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 能看出注釋了哪些代碼嗎?
如果用 //,結(jié)果會清晰很多:
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 的調(diào)用參數(shù):
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
{
其實(shí)這個修改發(fā)生在注釋里邊 (要增加上下文才能看到, diff -U 20,多一道手續(xù),降低了工作效率),對代碼行為沒有影響:
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 成員的定義,數(shù)組的初始化列表等等。
3. 函數(shù)聲明中的參數(shù)
如果函數(shù)的參數(shù)大于 3 個,那么在逗號后面換行,這樣每個參數(shù)占一行,便于 diff。以 muduo::net::TcpClient 為例:
class TcpClient : boost::noncopyable
{
public:
TcpClient(EventLoop* loop,
const InetAddress& serverAddr,
const string& name);
如果將來 TcpClient 的構(gòu)造函數(shù)增加或修改一個參數(shù),那么很容易從 diff 看出來。這恐怕比在一行長代碼里數(shù)逗號要高效一些。
4. 函數(shù)調(diào)用時的參數(shù)
在函數(shù)調(diào)用的時候,如果參數(shù)大于 3 個,那么把實(shí)參分行寫。以 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());
這樣一來,如果將來重構(gòu)引入了一個新參數(shù)(好吧,epoll_wait 不會有這個問題),那么函數(shù)定義和函數(shù)調(diào)用的地方的 diff 具有相同的形式(比方說都是在倒數(shù)第二行加了一行內(nèi)容),很容易肉眼驗(yàn)證有沒有錯位。如果參數(shù)寫在一行里邊,就得睜大眼睛數(shù)逗號了。
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[];
};
注意,初始化列表的順序必須和數(shù)據(jù)成員聲明的順序相同。
6. 與 namespace 有關(guān)的縮進(jìn)
Google 的 C++ 編程規(guī)范明確指出,namespace 不增加縮進(jìn)。這么做非常有道理,方便 diff –p 把函數(shù)名顯示在每個 diff chunk 的頭上。
如果對函數(shù)實(shí)現(xiàn)做 diff,chunk name 是函數(shù)名,讓人一眼就能看出改的是哪個函數(shù)。如下圖,紅色劃線部分。

如果對 class 做 diff,那么 chunk name 就是 class name。

diff 原本是為 C 語言設(shè)計(jì)的,C 語言沒有 namespace 縮進(jìn)一說,所以它默認(rèn)會找到“頂格寫”的函數(shù)作為一個 diff chunk 的名字,如果函數(shù)名前面有空格,它就不認(rèn)得了。muduo 的代碼都遵循這一規(guī)則,例如:
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 從第一列開始寫,不縮進(jìn)
// 函數(shù)的實(shí)現(xiàn)也從第一列開始寫,不縮進(jìn)。
Timestamp Timestamp::now()
{
struct timeval tv;
gettimeofday(&tv, NULL);
int64_t seconds = tv.tv_sec;
return Timestamp(seconds * kMicroSecondsPerSecond + tv.tv_usec);
}
相反,boost 中的某些庫的代碼是按 namespace 來縮進(jìn)的,這樣的話看 diff 往往不知道改動的是哪個 class 的哪個成員函數(shù)。
這個或許可以通過設(shè)置 diff 取函數(shù)名的正則表達(dá)式來解決,但是如果我們寫代碼的時候就注意把函數(shù)“頂格寫”,那么就不用去動 diff 的默認(rèn)設(shè)置了。另外,正則表達(dá)式不能完全匹配函數(shù)名,因?yàn)楹瘮?shù)名是上下文無關(guān)語法(context-free syntax),你沒辦法寫一個正則語法去匹配上下文無關(guān)語法。我總能寫出某種函數(shù)聲明,讓你的正則表達(dá)式失效(想想函數(shù)的返回類型,它可能是一個非常復(fù)雜的東西,更別說參數(shù)了)。更何況 C++ 的語法是上下文相關(guān)的,比如你猜 Foo<Bar> qux; 是個表達(dá)式還是變量定義?
7. public 與 private
我認(rèn)為這是 C++ 語法的一個缺陷,如果我把一個成員函數(shù)從 public 區(qū)移到 private 區(qū),那么從 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 了嗎?對此我也沒有好的解決辦法,總不能每個函數(shù)前面都寫上 public: 或 private: 吧?
對此 Java 和 C# 都做得比較好,它們把 public/private 等修飾符放到每個成員函數(shù)的定義中。這么做增加了信息的冗余度,讓 diff 的結(jié)果更直觀。
歡迎補(bǔ)充。
對 grep 友好的代碼風(fēng)格
操作符重載
C++工具匱乏,在一個項(xiàng)目里,要找到一個函數(shù)的定義或許不算太難(最多就是分析一下重載和模板特化),但是要找到一個函數(shù)的使用就難多了。不比 Java,在 Eclipse 里 Ctrl+Shift+G 就能找到所有的引用點(diǎn)。
假如我要做一個重構(gòu),想先找到代碼里所有用到 muduo::timeDifference 的地方,判斷一下工作是否可行,基本上惟一的辦法是grep。用 grep 還不能排除同名的函數(shù)和注釋里的內(nèi)容。這也說明為什么要用 // 來引導(dǎo)注釋,因?yàn)樵?grep 的時候,一眼就能看出這行代碼是在注釋里的。
在我看來,operator overloading 應(yīng)僅限于和 STL algorithm/container 配合時使用,比如 transform() 和 map<T,U>,其他情況都用具名函數(shù)為宜。原因之一是,我根本用 grep 找不到在哪兒用到了 operator-()。這也是 muduo::Timestamp 只提供 operator<() 而不提供 operator+() operator-() 的原因,我提供了兩個函數(shù) timeDifference 和 addTime 來實(shí)現(xiàn)所需的功能。
又比如,Google Protocol Buffers 的回調(diào)是 class Closure,它的接口用的是 virtual function Run() 而不是 virtual operator()()。
static_cast 與 C-style cast
為什么 C++ 要引入 static_cast 之類的轉(zhuǎn)型操作符,原因之一就是像 (int*) pBuffer 這樣的表達(dá)式基本上沒辦法用 grep 判斷出它是個強(qiáng)制類型轉(zhuǎn)換,寫不出一個剛好只匹配類型轉(zhuǎn)換的正則表達(dá)式。(again,語法是上下文無關(guān)的,無法用正則搞定。)
如果類型轉(zhuǎn)換都用 *_cast,那只要 grep 一下我就能知道代碼里哪兒用了 reinterpret_cast 轉(zhuǎn)換,便于迅速地檢查有沒有用錯。為了強(qiáng)調(diào)這一點(diǎn),muduo 開啟了編譯選項(xiàng) -Wold-style-cast 來幫助查找 C-style cast,這樣在編譯時就能幫我們找到問題。
一切為了效率
如果用圖形化的文件比較工具,似乎能避免上面列舉的問題。但無論是 web 還是客戶端,無論是 inline diff 還是 diff by lines 都不能解決全部問題,效率也不一定更高。
對于(2),如果想知道是誰在什么時候增加的 double z,在分行寫的情況下,用 git blame 或 svn blame 立刻就能找到始作俑者。如果寫成一行,那就得把文件的 revisions 拿來一個個人工比較,因?yàn)檫@一行 double x = 0.0, y = 1.0, z = -1.0; 可能修改過多次,你得一個個看才知道什么時候加入了變量 z。這個 blame 的 case 也適用于 3、4、5。
比如(6)改動了一行代碼,你還是要 scroll up 去找改的是哪個 function,人眼看的話還有“看走眼”的可能,又得再定睛觀瞧。這一切都是浪費(fèi)人的時間,使用更好的圖形化工具并不能減少浪費(fèi),相反,我認(rèn)為增加了浪費(fèi)。
另外一個常見的工作場景,早上來到辦公室,update 一下代碼,然后掃一眼 diff output 看看別人昨天動了哪些文件,改了哪些代碼,這就是一兩條命令的事,幾秒鐘就能解決戰(zhàn)斗。如果用圖形化的工具,得一個個點(diǎn)開文件 diff 的鏈接或點(diǎn)開新 tab 來看文件的 side-by-side 比較(不這么做的話看不到足夠多的上下文,跟看 diff output 無異),然后點(diǎn)擊鼠標(biāo)滾動頁面去看別人到底改了什么。說實(shí)話我覺得這么做效率不比 diff 高。
(待續(xù))