C++ Tips and Tricks


整理了下在C++工程代碼中遇到的技巧與建議。

0x00 巧用宏定義。

#define STRINGFY(X) #X

stringfy函數。處理shader文本,可以放在代碼中會語法高亮顯示,不會因為最終結果是字符串而不highlight。而且可以添加注釋,比如:

const char* str = STRINGFY(
    this // line_comment
    i/* block_comment */s
    a
    test
);

在處理后 const char* str = "this i s a test";,注意那個空格。
注意C/C++源碼的預處理流程,是先處理注釋的,// line_comment會被去掉,Java語言沒有預處理,所以在雙引號塊內需要\n換行,否則會把后面的文本全都注釋了。
還有塊注釋/* block_comment */,會被替換成一個空格,而不是直接去掉,為什么呢?
想想 int x = 0; 是合法語句,in/**/t x = 0;會編譯失敗就知道了。
翻譯階段(transplation phase)是這么說的
  Phase 3 2) Each comment is replaced by one space character
C++11 的 raw string 借鑒了 Python 的三引號。
  prefix(optional) R "delimiter( raw_characters )delimiter"
在 Phase 3 1)處理,也就是說,如果上面的括號的內容寫在 raw string 里,就不會被去掉的,因為定了起始、終止定界符。



  經常看見程序員用 enum 值,打印調試信息的時候又想打印數字對應的字符意思。見過有人寫這樣的代碼 if(today == MONDAY) return "MONDAY"; 一般錯誤代碼會有很多種,應該選用 switch-case 而不是 if-else。因為 if-else 最壞比較N次,switch-case 最壞是lgN次。這里用上宏,代碼簡介明了。而且也去掉了查找,直接索引到對應的字符串。

// from Android source frameworks/base/cmds/servicemanager/binder.c
#define NAME(n) case n: return #n
const char *cmd_name(uint32_t cmd)
{
    switch(cmd) {
        NAME(BR_NOOP);
        NAME(BR_TRANSACTION_COMPLETE);
        NAME(BR_INCREFS);
        NAME(BR_ACQUIRE);
        NAME(BR_RELEASE);
        NAME(BR_DECREFS);
        NAME(BR_TRANSACTION);
        NAME(BR_REPLY);
        NAME(BR_FAILED_REPLY);
        NAME(BR_DEAD_REPLY);
        NAME(BR_DEAD_BINDER);
    default: return "???";
    }
}

 

enum Day { SUNDAY = 0, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, COUNT }; const char* toString(Day day) { #define CODE(code) array[static_cast<uint32_t>(code)] = #code
    const char* array[COUNT]; CODE(SUNDAY); CODE(MONDAY); // ...
#undef CODE assert(0 <= day && day < COUNT); return array[day]; }

  接着有人說,數組每次運行會初始化一次啊。是的,可以用 static 變量達到僅初始化一次,生成的匯編代碼里,會設置一個bit位,用來檢查是否已初始化,而作相應的跳轉。
  static local variables 在C++11標准里明確指出:在多線程環境下,靜態變量的初始化只會出現一次。

If multiple threads attempt to initialize the same static local variable concurrently, the initialization occurs exactly once (similar behavior can be obtained for arbitrary functions with std::call_once).

Note: usual implementations of this feature use variants of the double-checked locking pattern, which reduces runtime overhead for already-initialized local statics to a single non-atomic boolean comparison.

  當然,如果想執行一段代碼或一個函數一次。即讓函數有 static 變量的效果,可不是也加 static 關鍵字了(static 函數表示作用域僅在該文件可見),可以這么寫:

void do_something() { std::cout<< "hello world\n"; } bool run_once() { do_something(); return true; } int main() { static bool unused = run_once(); return 0; }

這樣稍微包裝一下,就可以用了。利用 C++11 的 lambda 函數,可以不用額外寫函數啦!

static bool unused = []() -> bool { do_something(); return true; }();

最簡單的 lambda 函數申明 [](){}; 調用該函數就是 [](){}();
這里沒有使用任何數字和字母,怎么樣,新奇吧,好玩吧?

 

 

0x01 效率提升之用臨時寄存器變量減少成員變量讀寫次數。
  在 Gameloft 做游戲開發的時候,經常要考慮如何提升游戲的性能。iPhone iPad的硬件水平非常好,Android 的硬件參差不齊,所以游戲從 iOS 移植到 Android 需要考慮 Android 的感受。講一下我在游戲中遇到的一個問題。類中有一個數組 values,我們對它的計算結果 mTotal 作了緩存,這樣不用每幀都計算。

for(int i = 0; i < count; ++i)
    mTotal += values[i];

////////////////////////////////
 register int64_t total = 0;
for (int i = 0; i < count; ++i)
    total += values[i];
mTotal = total;

  分割線上下幾行代碼實現的功能是一樣的,但是下面的速度更快些。下面用了一個棧臨時變量(用了 register,也可能分配到一個寄存器),不用在每次循環中都讀寫成員變量,而是計算完后一次寫入。可能有些人還是不明白,可以嘗試自己反編譯成匯編代碼查看分析——不管讀還是寫,都經過先找到對象,再找到成員變量。



0x02 模擬其他語言字符串操作函數,startWith、split。

#include <string>
std::string haystack("malicious");
std::string needle("mali");
bool startWith(const std::string& haystack, const std::string& needle)
{
    return haystack.compare(0, needle.length(), needle) == 0;
}

C++ 里沒有提供 startWith 方法,所以 Java 程序員轉 C++ 的時候,處理字符串的時候,就很想找到這個函數。其他的函數還有trim,split等。其他轉語言的開發者都會有類似的經歷。上面用 haystack, needle 是套用 strstr 的參數名,很形象的比喻。man strstr 后,char *strstr(const char *haystack, const char *needle);。另外提一下,取 std::string 的長度,用 length() 或 size() 都可以,但最好用 length(),因為 size() 是 STL 里慣用的概念,以示區別。

#include <string>
#include <sstream>
#include <vector>

std::vector<std::string> split(const std::string& str, char delim)
{
    std::vector<std::string> elems;
    std::stringstream stream(str);
    std::string item;
    while(std::getline(stream, item, delim))
        if(!item.empty())  // skip empty token
            elems.push_back(item);
        
    return elems;
}

  字符串分割函數,如果你熟悉boost庫,也可以用boost提供的split。

#include <boost/algorithm/string.hpp>

std::vector<std::string> strs;
boost::split(strs, "string to split", boost::is_any_of("\t "));

 



0x03 scanf、printf 函數
  讀取一個字符串,遇到逗號停止,可以這么寫:scanf("%[^,]\n", str);。但如果是多個字符串,忽略掉之前的字符,怎么寫好呢?例如我想獲取"Taylor Swift"的姓,一般的就這么寫了,scanf("%s %s", temp, last_name); 這樣占用了一個臨時變量,改成 scanf("%s ", last_name); scanf("%s ", last_name); 的話,需要兩次調用 scanf 函數,最佳的答案是 scanf("%*s %s", last_name); 。是這樣的,如果你想忽略某個輸入,可以在%后用*。

  寫數據前,不能確定緩沖區的大小,該如何寫?printf 族函數,大家都知道是用來打印的,很少有人能說出它有返回值,並且返回值是寫入字符的個數。這里,返回值就派上用場了。
Calling std::snprintf with zero buf_size and null pointer for buffer is useful to determine the necessary buffer size to contain the output:

const char *fmt = "sqrt(2) = %f"; int sz = std::snprintf(nullptr, 0, fmt, std::sqrt(2)); std::vector<char> buf(sz + 1); // note +1 for null terminator
std::snprintf(&buf[0], buf.size(), fmt, std::sqrt(2));

  Visual C++ 先前沒有提供 snprintf 函數,取而代之的是 _snprintf, _snprintf_s。長得像,但是完成的功能與C語言標准有些出入。直到 Visual Studio 2015 才加進來。_snprintf 寫時溢出的時候不會寫結束的\0字符,_snprintf_s 改善了安全性,但是在溢出的時候返回 -1,而不是標准所要求的返回寫入的字符的長度。

#ifndef COMPILER_H_
#define COMPILER_H_

#include "stdio.h"
#include "stdarg.h"

/*
    Microsoft has finally implemented snprintf in VS2015 (_MSC_VER == 1900).

    Releases prior to Visual Studio 2015 didn't have a conformant implementation. 
    There are instead non-standard extensions such as _snprintf() (which doesn't 
    write null-terminator on overflow) and _snprintf_s() (which can enforce 
    null-termination, but returns -1 on overflow instead of the number of 
    characters that would have been written).
*/
#if defined(_MSC_VER) && _MSC_VER < 1900

#define snprintf c99_snprintf
#define vsnprintf c99_vsnprintf

inline int c99_vsnprintf(char *outBuf, size_t size, const char *format, va_list ap)
{
    int count = -1;

    if (size != 0)
        count = _vsnprintf_s(outBuf, size, _TRUNCATE, format, ap);
    if (count == -1)
        count = _vscprintf(format, ap);

    return count;
}

inline int c99_snprintf(char *outBuf, size_t size, const char *format, ...)
{
    int count;
    va_list ap;

    va_start(ap, format);
    count = c99_vsnprintf(outBuf, size, format, ap);
    va_end(ap);

    return count;
}


#endif

#endif /* COMPILER_H_ */


0x04 類型轉換。
  在 Gameloft 的日子里,每個月都會定期代碼檢查(Code Review),結果會通報給各位。很多人不情願寫 C++ 的類型轉換,因為它有點長,沒C語言的括號來的便捷。導致后來的我養成習慣,寫C++代碼時候,一律正確使用 C++的轉換,而且現在的編譯器都會有補全提示功能,不需要敲完整個關鍵字了。而且在游戲里,避免使用C++ 的 RTTI 和 exception 功能,RTTI 開銷大,C++ 以前的異常處理簡直就是一雞肋。這里,矛盾就出現了,dynamic_cast 按照前者(C++ cast)應該使用,按照后者(用了RTTI)是不應該使用的。於是就有了下面的代碼,debug 版調試時用來檢測正確的類型,在 release 版本強轉。很多對象繼承於 Entity,於是 Creature 類,就用了所謂的 CREATURE_CAST 宏,其實就是
#define CREATURE_CAST(entity)  downcast<Creature*>(entity)

template <typename To, typename From>
To downcast(From p)
{
#ifdef DEBUG
    To to = dynamic_cast<To>(p);
    assert(to != nullptr);
    return to;
#else
    return static_cast<To>(p);
#endif
}

 

0x05 跳轉新玩法。
對於 int a, b 兩個值,如果都滿足的話,執行某語句。大概很多人都會這么寫: if(a >0 && b > 0) do_something();
我在看代碼的時候,看到了一種很贊的寫法,忘記出處了。寫法是: if((a|b) > 0) do_something();
相比而言,少了一條跳轉指令。你可以感受一下。
想到這,還要提醒一下,不要在函數里寫 if(isGood) return true; else return false; 這樣的代碼了,直接寫 return isGood; 就好了。


0x06  char* 與 std::string 之間的遷移。

<string.h>       =>  <string>
strlen(str)    =>  str.size()
strcmp(s1, s2)   =>  s1.compare(s2)
strcat(s1, s2)   =>  s1 += s2
strcpy(s1, s2)   =>  s1 = s2
strstr(s1, s2)  =>  s1.find(s2)
strchr(str, ch)   =>  str.find_first_of(ch)
strrchr(str, ch)  =>  str.find_last_of(ch)
strspn(s1, s2)   =>  s1.find_first_not_of(s2)
s1 = strdup(s2)  =>  s1 = s2



0x06 更快的循環。
  哪有什么更快的循環,又不是並行計算。其實,我要說得是一些寫的不好的代碼會導致速度降下來。新手寫代碼,讓代碼達到功能需求就完事,很少會像工匠雕琢一樣,怎樣寫出更好的代碼。我見過這么寫代碼的 for(int i = 0; i < strlen(str); i++) ,功能上正確,但是:其一,如果 str 過長,效率就不理想,因為它每次循環會計算長度;其二,雖然編譯器可能對 i++ 優化,但是最好還是用前綴自增 ++i 吧;其三,類型不對的整數比較,i是 int 型,而 strlen 返回 size_t 型。然后他修改成這樣,size_t len = strlen(str); for(size i = 0; i < len; ++i) 將長度提取出來,用了一個額外的變量。然后我告訴他,可以直接這么寫的: for(size i = 0, len = strlen(str); i < len; ++i) 或者 for(const char* p = str; *p != '\0'; ++p) 。
  STL 數據結構在讀取的時候,小心調用構造函數的開銷。項目里有一人很喜歡帶冒號的 for 循環(即 for_each),這樣很好,符合 C++ 的信息隱藏的思想。我只是要遍歷一遍而已,你不用很明顯地告訴我是從第一個自增迭代到最后一個。可是他寫的時候,是這樣的:
std::vector<Creature> creatures;
for(const Creature creature: creatures)
    creature.roar();

乍一看,沒什么啊。我提醒了他,這里應該用引用,for(const Creature& creature: creatures),可以避免調用復制構造函數的開銷,就跟函數的傳值與傳引用一樣。


0x07 查找的時候,有沒有類似 std::binary_search 一樣的函數,我需要的是返回迭代器,而不是僅僅告訴我查找的元素是否存在。
  STL 里面好像沒有這樣的函數,不過你可以用上 std::lower_bound, std::upper_bound 或 std::equal_range。
template<class Iter, class T>
Iter binary_find(Iter begin, Iter end, T value)
{
    // Finds the lower bound in at most log(last - first) + 1 comparisons
    Iter it = std::lower_bound(begin, end, value);
    if (it != end && !(value < *it))
        return i; // found it
    else
        return end; // not found
}

  STL 取 find 名的函數是返回迭代器的。
  注意,這里是模板函數,不要用 *it == value,要用 !(value < *i)。原因是 std::lower_bound 用的是 < ,即嚴格的弱序關系。這里的類型 T 可以沒有相等==的判斷。相等(equality)與等價(equivalence)是不能搞混淆的。可以查看 Scott Meyers 的書 Effective STL 的 19 條,有關 equality 與 equivalence 的區別。
  我在上面也栽倒過。unordered_map (舊時叫 hash_map,但未納入標准)的類模板聲明如下:

template<
    class Key,
    class T,
    class Hash = std::hash<Key>,
    class KeyEqual = std::equal_to<Key>,
    class Allocator = std::allocator< std::pair<const Key, T> >
> class unordered_map;

  我需要完成反向查找,即原先是通過key找value,現在是通過value找key。std::unordered_map<vec3i, int> table; 由於 vec3i 是自己的類,沒有默認的 hash 函數,需要自己提供。之前重載關於 tuple 的比較函數,被我寫成了

class IndexCompare
{
public:
    bool operator()(const vec3i& a, const vec3i& b) const
    {
        return a.i < b.i || a.j < b.j || a.k < b.k;
    }
};

而正確的應該是這樣的,采用"skip while equal, then compare"的策略。std::vector 有重載 operator < 操作符,也可以直接拿來用。

class IndexCompare
{
public:
    bool operator()(const vec3i& a, const vec3i& b) const
    {
        // Operator < and strict weak ordering
        if(a.i != b.i) return a.i < b.i;
        if(a.j != b.j) return a.j < b.j;
        if(a.k != b.k) return a.k < b.k;

        return false;
    }
};

 

0x08 assert/static_assert 斷言錯誤,當斷不斷,反受其亂。
  寫程序,更多的時間是花在調試上。這里為什么會出錯,那里為什么沒走到?啊啊啊。在可能出錯的地方寫上斷言,可以避免錯誤多次傳播到其他地方。也就是上游犯的錯,下游某處運行不暢被發現了,你以為是下游的問題,稍微堵了一下。好像沒事了,你欣喜地以為可以關上一個bug了,下次又冒出下游的其他地方出問題了,其實原本你就不應該讓上游的錯誤漂流這么遠。《史記》有曰”當斷不斷,反受其亂”,適當地添加斷言,是有好處的。為了讓錯誤及早發現,能用 static_assert 的地方絕不用 assert,即編譯時可檢查錯誤的地方絕不推遲到運行時。可以讀一下參考的快速失敗(fail fast),出錯就不應該運行下去。
  C++ 於 C 而言,強類型語言,即很強調類型的安全性。項目中有人犯過這樣的錯,起先用了一個 int value; 存儲能量值,計算的時候用了 abs(value); 后來發現,value 會參與乘除運算,更應該用浮點數表達,於是將類型改成了 float value; 編譯也沒任何錯誤。可是他忘記了修改 abs(value) 為 absf(value),編譯器作了隱式轉換,將 float 轉成 int 再調用 int 版的 abs 函數,導致他 debug 了很長時間。其實利用 C++ 的模板就可以避免此類錯誤,#include <cmath> 而不是 #include <math.h> ,然后調用 std::abs() 實現無縫切換。
  要關注編譯器發出的 warning,不要忽略,因為這也可能是 bug 隱患。舊版本的 Visual Studio 對 C++ 標准支持的不好(好事是微軟團隊在慢慢改進),GCC/Clang 編譯器可能檢查出更多的錯誤。


參考:
Scott Meyers 的 Effective C++ 系列叢書
LLVM 的編碼規范 http://llvm.org/docs/CodingStandards.html
Martin Fowler 的 Fail Fast


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM