• 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语言没有预处理,所以在双引号块内需要 换行,否则会把后面的文本全都注释了。
    还有块注释/* 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
    ";
    }
    
    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("	 "));



    0x03 scanf、printf 函数
      读取一个字符串,遇到逗号停止,可以这么写:scanf("%[^,] ", 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 写时溢出的时候不会写结束的字符,_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 != ''; ++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

  • 相关阅读:
    boost库常用库介绍
    boost介绍
    vs2019+win10配置boost库
    交互式多媒体图书平台的设计与实现
    47.全排列 2
    46.全排列
    基于VSCode的C++编程语言的构建调试环境搭建指南
    码农的自我修养之必备技能 学习笔记
    工程化编程实战callback接口学习笔记
    Erlang模块inet翻译
  • 原文地址:https://www.cnblogs.com/Martinium/p/cpp_tips_and_tricks.html
Copyright © 2020-2023  润新知