靜態程式碼分析工具可簡化編碼過程,檢測出錯誤並幫助修復。PVS-Studio 是一個用於 C/C++ 的靜態程式碼分析工具。該團隊檢測了 200 多個 C/C++ 開源專案,包括了 Unreal Engine、Php、Haiku、Qt 和 Linux 核心等知名專案。於是他們每天分享一個錯誤案例,並給出相應建議。伯樂線上翻譯組正在翻譯這個系列,今天是第二篇。
這個 bug 是在 Unreal Engine 4 的原始碼中發現的。
錯誤程式碼:
1 2 3 4 5 6 7 8 9 10 |
void FSlateNotificationManager::GetWindows( TArray< TSharedRef<SWindow> >& OutWindows) const { for( auto Iter(NotificationLists.CreateConstIterator()); Iter; Iter++ ) { TSharedPtr<SNotificationList> NotificationList = *Iter; .... } } |
解釋:
如果不讀標題的話,你可能很難發現這段程式碼裡的問題。第一眼看上去這段程式碼完全正確,其實它並不完美。沒錯,我指的是後自增運算子 Iter++ 。 我們應該儘量使用前自增運算子而不是後自增運算子,即用 ++ Iter 代替 Iter++ 。 為什麼要這麼做,有什麼有實際價值?下面我會詳細解釋。
正確程式碼:
1 2 3 4 5 6 7 8 9 10 |
void FSlateNotificationManager::GetWindows( TArray< TSharedRef<SWindow> >& OutWindows) const { for( auto Iter(NotificationLists.CreateConstIterator()); Iter; ++Iter) { TSharedPtr<SNotificationList> NotificationList = *Iter; .... } } |
建議:
字首和字尾形式之間的區別是眾所周知的。我希望它們內部結構的區別(告訴了我們運演算法則)大家也是清楚的。如果你有使用過運算子過載的話,肯定已經意識到了。沒有用過的話,我在這兒簡單地解釋一下(用過運算子過載的可以跳過下面關於運算子過載的例子)。
前自增運算子改變了物件的狀態並返回物件改變後的狀態,不需要建立臨時物件。下面是前自增運算子的例子:
1 2 3 4 5 |
MyOwnClass& operator++() { ++meOwnField; return (*this); } |
後自增運算子也改變了物件的狀態但是返回的是物件改變前的狀態,並且需要建立一個臨時物件。下面是後自增運算子過載的例子:
1 2 3 4 5 6 |
MyOwnClass operator++(int) { MyOWnCLass tmp = *this; ++(*this); return tmp; } |
看到上面這段程式碼,你會發現有一個額外的操作,就是要建立一個臨時物件,在實踐中這點太重要了!
現在的編譯器做程式碼優化的時候非常智慧,如果沒有用處,是不會隨便建立臨時物件的。這就是為什麼在釋出版中我們很難發現 i++ 和 ++ i 的區別。
但是在除錯模式下進行程式除錯的時候就是另一回事了,這時候你會看到效能上有很大差別。
舉個例子,在這篇文章中,有一些例子可以估計除錯版本中使用前自增和後自增運算子的程式碼執行時間,我們可以看到使用字尾形式所用時間幾乎是字首的四倍。
有人會說:”那又怎麼樣?反正釋出版都是一樣的。”,這種想法說對也對說不對也不對。通常我們會花更多的時間做單元測試和除錯程式,所以大多數時間都在除錯版本下工作,誰也不想浪費時間在那兒等吧?
關於“對於迭代器,我們是否應該用前自增運算子(++i)來代替後自增運算子(i++)?”這個問題,我想認真地回答: “是的,真應該這麼做”。 你會發現在除錯版本中速度大大提升。 如果迭代器很複雜的話,這麼做的好處更是顯而易見了。
參考資料(閱讀推薦):
- Is it reasonable to use the prefix increment operator ++it instead of postfix operator it++ for iterators?
- Pre vs. post increment operator – benchmark
這個錯誤是用靜態程式碼分析工具 PVS-Studio 發現的,錯誤資訊為:V803 效能下降。 如果iter是迭代器的話,使用前自增運算子會更高效,使用 ++iter 代替 iter++.