GoogleCpp风格指南 5) 其他特性_part2

5.10 前置自增和自减 Preincrement and Predecrement

Tip 对于迭代器iterator和其他模板对象template object使用前缀形式(++i)的自增, 自减运算符;

定义: 

对于变量在自增(++i 或 i++)或自减(--i 或 i--)后, 表达式的值没有被用到的情况下, 需要确定到底是使用前置还是后置的自增(自减);

优点: 

不考虑返回值的话, 前置pre自增(++i)通常要比后置post自增(i++)效率更高; 因为后置自增(自减)需要对表达式的值 i 进行一次拷贝; 如果i是迭代器或其他非数值 non-scalar类型, 拷贝的代价是比较大的; 既然两种自增方式实现的功能一样, 为什么不总是使用前置自增呢?

缺点: 

在C开发中, 当表达式的值未被使用时, 传统的做法还是使用后置自增, 特别是在 for循环中; 有些人觉得后置自增更加易懂, 因为这很像自然语言, 主语subject(i)在谓语动词precede(++)前; 

[C语言中没有class类型, 基本上POD不必在意前置或后置的效率区别]

结论: 

对简单数值scalar(非对象non-object), 两种都无所谓; 对迭代器和模板类型, 使用前置自增(自减);


5.11 const的使用 Use of const

Tip 强烈建议在任何可能的情况下都要使用 const; [Add] c++11中, constexpr对于某些const使用情况是更好的选择; [http://en.cppreference.com/w/cpp/language/constexpr] <<<

定义: 

在声明的变量或参数前preceded加上关键字 const用于指明变量值不可被篡改(如 const int foo); 为类中的函数加上 const限定符qualifier表明该函数不会修改类成员变量的状态(如 class Foo { int Bar(char c) cosnt; }; )

优点: 

大家更容易理解如何使用变量; 编译器可以更好地进行类型检测; 相应地, 也能生成更好的代码; 人们对编写正确的代码更加自信, 因为他们知道所调用的函数被限定了能或不能修改变量值; 即使是在无锁的多线程编程中without locks in multi-threaded, 人们也知道什么样的函数是安全的;

缺点: 

const是入侵性viral的: 如果你向一个函数传入 const变量, 函数原型声明中也必须对应 const参数(否则变量需要 const_cast类型转换), 在调用库函数是显得尤其麻烦;

结论:

const变量, 数据成员, 函数和参数为编译时类型检测增加了一层保障: 便于尽早发现错误; 因此, 我们强烈建议在任何可能的情况下使用 const;

- 如果函数不会修改传入的引用或指针类型参数, 该参数应声明为const;

- 尽可能将函数声明为const; 访问函数几乎总是const; 其他不会修改任何数据成员, 没有调用非const函数, 不会返回数据成员非const指针或引用的函数也应声明成const;

- 如果数据成员在对象构造之后不再发生变化, 可将其定义为const;

[Remove] 然而, 也不要发疯似的使用const; 像 const int* const* const x; 就有些过了, 虽然它非常精确地描述了常量 x; 关注真正有帮助一样的信息: 前面的例子写成 const int** x就够了; [内容不可变] <<<

关键字 mutable可以使用, 但是在多线程中是不安全的, 使用时首先要考虑线程安全性;


const的位置 Where to put the const:

有人喜欢 int const *foo形式, 不喜欢 const int* foo; 他们认为前者更一致因此可读性也更好: 遵循了const总位于其描述的对象之后的原则; 但是一致性原则不适用于此, 由于多数const表达式只有一个const, 而且应用的是一个值, 很少有深层嵌套的指针表达式few deeply-nested pointer expressions; "不要过度使用"的声明可以取消大部分你原本想保持的一致性; 将const放在前面才更易读, 因为在自然语言中形容词adjective(const)是在名词noun(int)之前;

这是说, 我们提倡但不强制const在前; 但要保持代码的一致性; (译注, 就是不要在一些地方把const写在类型前面, 在其他地方又写在后面, 要确定一种写法, 然后保持一致);


[Add]

使用constexpr Use of constexpr

C++11中, 使用constexpr来定义true的常量或确保常量初始化constant initialization; 

定义:

一些变量可以被声明为constexpr, 表明变量是true的常量, e.g. 在编译/链接时是固定的; 一些函数和cotr可以被声明为constexpr, 让它们可以在定义一个constexpr变量时被使用;

优点:

使用constexpr定义浮点数floating-point表达式常量, 而不是字面量literal定义, 用户定义的类型的定义和函数调用function call的常量的定义;

缺点:

把某些东西定义为constexpr可能会在之后导致一些迁移migration问题, 或许不得不回退回去downgraded; 目前的对于constexpr函数和ctor的限制规定restriction可能会在这些定义中产生些隐晦的替代方案workaround;

结论:

constexpr定义可以在一些接口的const部分给出健壮的规格robust specification; 使用constexpr来指定真实常量true constants以及支持函数的定义; 使用constexpr来防止复杂的函数定义; 不要使用constexpr来强制内联 inline; [http://stackoverflow.com/questions/14391272/does-constexpr-imply-inline ]

<<<


5.12 整型 Integer Types

Tip C++内建整型中, 仅使用 int; 如果程序中需要不同大小的变量, 可以使用 <stdint.h>中长度精确precise-width的整型, 如 int16_t; [http://www.cplusplus.com/reference/cstdint/ ]

[Add] 如果你的变量表示了一个可以变大或者等于2^31(2GiB)的值, 使用一个64-bit类型, 比如int64_t; 记住即使你的值对于int来说不会过大, 它还是可能在一些中间计算中需要一个更大的类型; 如果不确定, 就选用更大的类型; <<<

定义: 

C++没有指定整型的大小; 通常人们假定 short是16位, int是32位, long是32位, long long是64位; (bits)

优点: 

保持声明统一性Uniformity;

缺点: 

C++中整型大小因编译器和体系结构architecture的不同而不同;

结论:

<stdint.h>定义了 int16_t, uint32_t, int64_t等整型, 在需要确保guarantee 整型大小时可以优先preference使用它们代替 short, unsigned long long等; 在C整型中, 只使用 int; 在合适的情况下, 推荐使用标准类型如 size_tptrdiff_t;

如果已知整数不会太大, 我们常常会使用 int, 如循环计数loop counter; 在类似的情况下使用原生类型plain old int; 你可以认为int至少为32位, 但不要认为它会多于32位; 如果需要64位整型, 用 int64_t或 uint64_t;

对于大整数, 使用 int64_t;

不要使用 uint32_t等无符号整型, 除非有正当valid 理由, 比如在表示一个位组bit pattern而不是一个数值, 或是需要定义二进制补码溢出overflow modulo 2^N; 尤其是不要为了指出数值永远不会为负, 而使用无符号类型; 相反, 你应该用断言来保护数据;

<<<

[Add] 如果你的代码是一个返回大小的容器, 确保使用一个可适应accommodate任何使用的可能性的数据类型; 不确定的时候使用一个更大的类型而不是较小的类型;

当转化整型的时候要小心; 整型转换和提升promotion会引起一些反直觉non-intuitive的行为; 

<<<


关于无符号整数 On Unsigned Integers:

有些人, 包括一些教科书作者, 推荐使用无符号整型表示非负数; 这种做法试图达到自我文档化self-documentation; 但是在C语言中, 这一优点被由其导致的bug所淹没outweighed; 看看下面的例子:

1
for  (unsigned  int  i = foo.Length()-1; i >= 0; --i)  //...

上述循环永远不会退出! 有时gcc会发现该bug并报警, 但大部分情况下都不会; 类似的bug还会出现在比较有符号变量和无符号变量时; 主要是C的类型提升机制会导致无符号类型的行为出乎你的意料; 

因此, 使用断言来指出变量为非负数, 而不要使用无符号类型;


5.13 64位下的可移植性 64-bit Portability

Tip 代码应该对64位和32位系统友好; 处理打印printing, 比较comparison, 结构体对齐structure alignment时应该切记;

- 对于某些类型, printf()的指示符在32位和64位系统上可移植性不是很好; C99标准定义了一些可移植的格式化指示符; 不幸的是, MSVC7.1并非全部支持; 而且标准中也有所遗漏, 所以有时我们不得不自己定义一个丑陋的版本(头文件inttype.h仿标准风格):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// printf macros for size_t, in the style of inttypes.h
#ifdef _LP64
#define __PRIS_PREFIX "z"
#else
#define __PRIS_PREFIX
#endif
 
// Use these macros after a % in a printf format string
// to get correct 32/64 bit behavior, like this:
// size_t size = records.size();
// printf("%"PRIuS"\n", size);
#define PRIdS __PRIS_PREFIX "d"
#define PRIxS __PRIS_PREFIX "x"
#define PRIuS __PRIS_PREFIX "u"
#define PRIXS __PRIS_PREFIX "X"
#define PRIoS __PRIS_PREFIX "o"

check table:

Type DO NOT use DO use Notes
void * (or any pointer) %lx             %p              
int64_t             %qd%lld             %"PRId64"              
uint64_t             %qu%llu%llx             %"PRIu64"%"PRIx64"              
size_t             %u             %"PRIuS"%"PRIxS"             C99 specifies %zu            
ptrdiff_t             %d             %"PRIdS"             C99 specifies %td            


Note PRI*宏会被编译器扩展concatenated为独立字符串; 因此如果使用非常量的格式化字符串, 需要将宏的值而不是宏名插入格式中; 使用PRI*宏同样可以在 %后包含长度指示符,etc; 例如: printf("x = %30"PRIuS"\n", x); 在32位Linux将被展开为: printf("x = %30" "u" "\n", x); 编译器当成: printf("x = %30u\n", x);处理; (译注: 这在MSVC6.0上不行, VC6编译器不会自动把引号间隔的多个字符串连接成一个长字符串);

- 记住 sizeof(void *) != sizeof(int); 如果需要一个指针大小的整数要用 intptr_t; [不同编译器, 系统不一样]

- 要非常小心地对待结构体对齐, 尤其是要持久化存储到磁盘上的结构体; (译注: 持久化--将数据按字节流顺序保存在磁盘文件或数据库中); 在64位系统中, 任何含有 int64_t/uint64_t成员的类/结构体, 缺省都以8字节在结尾对齐; 如果32位和64位代码要共用持久化在磁盘上的结构体; 需要确保两种体系结构下的结构体对齐一致;(packed) 大多数编译器都允许调整结构体对齐; gcc中可使用 __attribute__((packed)); MSVC则提供了 #pragma pack()和 __deslspec(align());

(译注: 解决方案的项目属性栏里也可以直接设置) [VS的项目--solution] 

[http://en.wikipedia.org/wiki/Data_structure_alignment ]

- 创建64位常量时使用 LL或 ULL作为后缀suffixes, 如:

1
2
int64_t my_value = 0x123456789LL;
uint64_t my_mask = 3ULL << 48;

- 如果你确实需要32位和64位系统具有不同代码, 可以使用 #ifdef _LP64指令在代码变量中区分 32/64位代码; (尽量不要这么做, 如果非用不可, 尽量使修改局部化);

[http://stackoverflow.com/questions/685124/how-to-identify-a-64-bit-build-on-linux-using-the-preprocessor 

1
2
3
#if defined(__LP64__) || defined(_LP64)
#define BUILD_64   1
#endif

]


5.14 预处理宏 Preprocessor Macros

Tip 使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量替代之;

宏意味着你和编译器看到的代码是不同的; 这可能会导致异常行为, 尤其因为宏具有全局作用域

值得庆幸的是, C++中, 宏不像在C中那么必不可少; 以往用宏展开性能关键performance-critical的代码, 现在可以用内联函数替代; 用宏表示的常量可以被 const变量代替; 用宏"缩写"长变量的别名abbreviate可被引用代替reference; [以及typedef]; 用宏进行条件编译...这个, 千万别这么做, (#define防止头文件重复包含当然是个特例) 会令测试更加痛苦;

宏可以做一些其他技术无法实现的事情, 在一些代码库(尤其是底层库中)可以看到宏的某些特性(如用#字符串化stringifying, 用##连接concatenation等); 但在使用前, 仔细考虑一下能不能不使用宏达到同样的目的; [Hack: private和public]

下面给出的用法模式可以避免使用宏带来的问题; 如果要用宏, 尽可能遵守:

- 不要在 .h文件中定义宏;

- 在马上要使用时才进行 #define, 使用完要立即 #undefine;

- 不要只是对已经存在的宏使用 #undef, 选择一个不会冲突的独特名称;

- 不要试图使用展开后会导致C++构造不稳定的宏, 否则至少要附上文档说明其行为; [不要在构造相关代码使用宏?]

- 最好不要使用 ##来产生 function/class/variable的名字;


5.15 0 and nullptr/NULL

Tip 整数用 0, 实数用 0.0, 指针用 NULL或nullptr, 字符chars(串)用 '\0'; 

整数用0, 实数用0.0, 这一点毫无争议controversial;

对于指针(地址值), 到底是使用0还是NULL/nullptr,

[Remove]Bjarne Stroustrup建议使用最原始的0; 我们建议使用看上去像是指针的 NULL;  [C++11: nullptr]<<<

[Add] 对允许C++11的项目, 使用nullptr, C++03项目使用NULL, 看起来比较像个指针; <<<

事实上一些C++编译器(如gcc4.1.0)对 NULL进行了特殊的定义, 可以给出有用的警告信息, 尤其是 sizeof(NULL) sizeof(0)不相等的情况;   

字符(串)用 '\0', 不仅类型正确而且可读性好; [http://bbs.csdn.net/topics/390615761]


5.16 sizeof

Tip 尽可能用 sizeof(varname)代替 sizeof(type);

使用 sizeof(varname)是因为当代码中变量类型改变时会自动更新; 某些特定情况下 sizeof(type)或许有意义, 比如管理external或internal数据类型变量的代码, 而没有方便的C++类型; 但还是要尽量避免, 因为它会导致变量类型改变后不能同步;

1
2
Struct data;
memset (&data, 0,  sizeof (data));

WARNING 

1
memset (&data, 0,  sizeof (Struct));

[Add]

Other

1
2
3
4
if  (raw_size <  sizeof ( int )) {
   LOG(ERROR) <<  "compressed record not big enough for count: "  << raw_size;
   return  false ;
}

<<<


[Add]

auto

使用auto来避免类型名字杂乱clutter; 当有助于可读性的时候继续使用明显的manifest类型声明, 除了本地local变量之外不要使用auto;

定义:

C++11中, 一个由auto指定类型的变量会给出符合初始化它的表达式的类型; 可以使用auto来用copy初始化initialize它, 或者绑定bind一个引用;

[http://en.cppreference.com/w/cpp/language/auto]

1
2
3
4
vector<string> v;
...
auto  s1 = v[0];   // Makes a copy of v[0].
const  auto & s2 = v[0];   // s2 is a reference to v[0].

优点:

C++类型名称有时候很长而且笨重cumbersome, 特别是在模板或名字空间中:

1
sparse_hash_map<string,  int >::iterator iter = m.find(val);

返回值很难读懂, 蒙蔽obscure了语句的原本意图; 改为:

1
auto  iter = m.find(val);

更易读懂;

没有auto的话我们有时不得不将一个类型名字在一个表达式内写两遍, 对于阅读者来说没有意义:

1
diagnostics::ErrorStatus* status =  new  diagnostics::ErrorStatus( "xyz" );

使用auto让中间intermediate变量的使用更合理, 减少了显式书写类型的负担; 

缺点:

有时候变量是manifest的会更清晰, 特别是变量初始化依赖于我们之前声明的东西; 像这样的表达式:

1
auto  i = x.Lookup(key);

如果x是几百行之前声明的, i的类型可能不够明显;

程序员不得不去理解auto和const auto&之间的区别, 有时候他们会在没有意识到的时候拿到一份copy;

auto和C++11 brace-initialization之间的交互interaction可能会令人混淆; 声明:

1
2
auto  x(3);   // Note: parentheses.
auto  y{3};   // Note: curly braces.

它们表示不同的东西, x是个int, 但y是一个std::initializer_list<int>; 相同情况会发生在其他普通隐式代理normally-invisible proxy类型上; 

如果一个auto变量被用作接口的一部分, e.g. 在头文件中作为一个const, 程序员可能只是为了改变它的值而改变它的类型, 导致没有想到的一系列API的彻底radical改变;

结论:

auto只对本地变量开放; 不要在文件范围file-scopye或名字空间范围namespace-scope中对变量, 或类成员使用auto; 永远不要用大括号初始化列表braced initializer list初始化一个auto类型auto-typed变量; 

auto关键字也用在C++feature无关的地方: 它作为一种新的函数声明的语法的一部分, 尾随返回值类型 trailing return type; trailing return type只在lambda表达式中被允许使用;


Braced Initializer List

braced initializer lists; [http://en.cppreference.com/w/cpp/language/list_initialization]

在C++03, 聚合类型aggregate type(没有ctor的数组和结构体)可以用braced initializer list初始化; 

1
2
struct  Point {  int  x;  int  y; };
Point p = {1, 2};

C++11中, 这个语法被普遍化generalized了, 任何一个对象类型都可以用braced initializer list来初始化, 作为一个braced-init-list的C++语法grammar; 这里有几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Vector takes a braced-init-list of elements.
vector<string> v{ "foo" "bar" };
 
// Basically the same, ignoring some small technicalities.
// You may choose to use either form.
vector<string> v = { "foo" "bar" };
 
// Usable with 'new' expressions.
auto  p =  new  vector<string>{ "foo" "bar" };
 
// A map can take a list of pairs. Nested braced-init-lists work.
map< int , string> m = {{1,  "one" }, {2,  "2" }};
 
// A braced-init-list can be implicitly converted to a return type.
vector< int > test_function() {  return  {1, 2, 3}; }
 
// Iterate over a braced-init-list.
for  ( int  i : {-1, -2, -3}) {}
 
// Call a function using a braced-init-list.
void  TestFunction2(vector< int > v) {}
TestFunction2({1, 2, 3});

一个用户定义的类型也可以使用std::initializer_list<T> [http://en.cppreference.com/w/cpp/utility/initializer_list] 定义一个ctor或assignment operator, 会从braced-init-list自动创建;

1
2
3
4
5
6
7
8
9
10
11
12
13
class  MyType {
  public :
   // std::initializer_list references the underlying init list.
   // It should be passed by value.
   MyType(std::initializer_list< int > init_list) {
     for  ( int  i : init_list) append(i);
   }
   MyType& operator=(std::initializer_list< int > init_list) {
     clear();
     for  ( int  i : init_list) append(i);
   }
};
MyType m{2, 3, 5, 7};

最后 brace initialization也能调用普通的数据类型的ctor, 即使没有std::initializer_list<T>构造函数;

1
2
3
4
5
6
7
8
9
10
11
double  d{1.23};
// Calls ordinary constructor as long as MyOtherType has no
// std::initializer_list constructor.
class  MyOtherType {
  public :
   explicit  MyOtherType(string);
   MyOtherType( int , string);
};
MyOtherType m = {1,  "b" };
// If the constructor is explicit, you can't use the "= {}" form.
MyOtherType m{ "b" };

Note 永远不要把一个braced-init-list分配给一个auto的本地变量; 在单个元素的case中, 其意义可能会混淆:

Bad:

1
auto  d = {1.23};         // d is a std::initializer_list<double>

Good:

1
auto  d =  double {1.23};   // Good -- d is a double, not a std::initializer_list.

参见 Braced Initializer List Format;

<<<

---TBC---YCR

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值