博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Google C++ Style Guide在C++11普及后的变化
阅读量:6608 次
发布时间:2019-06-24

本文共 7072 字,大约阅读时间需要 23 分钟。

转 http://www.cnblogs.com/chen3feng/p/5972967.html?from=timeline&isappinstalled=0&lwfrom=user_dingfriend

 

一般比较规范的项目都有一个代码规范,Google C++ Style Guide(以下简称GCSG)是比较流行的C++代码规范,为什么我会分析它?因为我们现在就在用。

 

C++代码规范一般有两个方向,一个方向是很保守,基本把C++降级回c with classes的年代。我记得前几年我在某公司某项目中时,曾有领导建议代码规范中不要使用STL。还有个团队,老大禁用STL,于是组员把VC的STL代码扒过来改一下名字,比如vector改为Array,map改为Map,begin改为Begin,然后就允许用了。

另一种是偏前卫的方式,的应该算是其中的代表。C++之父搞的也算是这个流派。

 

GCSG算是这两者的折中,也就是说,在比较“土”的使用者看来,还是比较时尚的。在比较前卫的使用者看来,却偏保守,即使在业界其他大公司中算比较保守的,比如和和比起来。

 

前几年一个毛子程序员就狠狠地吐槽了一把:,原因是这哥们是boost爱好者,自称boost programmer,你就知道它属于什么流派了。他在多达6次收到google招聘人员的联系后怒了,写文章发泄了一把,还引起了负责人撕逼:。

扯得有点远了……

 

整体来说,GCSG还算是比较实用的,很详细,覆盖面很广,而且还有一个利器cpplint.py来搭配,方便实时自动检查。

GCSG的另一个好处是更新很及时,自从2008年以来,已经更新了好几百次。

C++11刚确定的时候,Google C++ Style Guide就做了更新,当时是这么写的:

只能使用批准的特性,然后来了一句,“目前,只批准了auto”。 

又过去五年了,语言和编译器都有了很大的进展,C++14标准发布了,C++17标准也在制定中,gcc/clang等的跟进也都比较及时,所以Google也与时俱进地更新了代码规范。

上周末在手机上把最新版的GCSG看了一遍,发现还是有不少明显的变化值得讲一下的。

闲话不提,下面逐条分析:

旧条款的更新

前向声明

前向声明是指一个使用类型时,如果只需要它的指针,引用,返回值,参数,而没有实际实例化对象时,可以用 class Foo; 这样的语法,避免包含其定义所在的头文件。

旧的代码规范里,鼓励使用前向声明,好处是减少依赖加快编译速度,减少代码修改时的重新编译,隐藏实现细节。

在新的规范里,已经改为了尽量避免使用前向声明,需要时直接包含其定义的头文件即可。原因:

  • 容易出现不一致,比如引发错误
    •  // b.h:

      1
      2
      3
      4
      5
      6
      7
      8
      struct 
      B {};
      struct 
      D : B {};
       
      // good_user.cc:
      #include "b.h"
      void 
      f(B*);
      void 
      f(
      void
      *);
      void 
      test(D* x) { f(x); }  
      // calls f(B*)
  • 妨碍接口升级,比如一个类,原来觉的名字不好或者命名空间不恰当,改为了新名字或者换了命名空间,如果代码中用了前向声明,就需要修改所有用到的地方。而如果不用,就可以用typedef或者using之类的方式兼容旧代码。
  • // 旧代码:class OldClassName {};// 旧代码:class NewClassName {};__attribute__((__deprecated("这个类名废弃了,请改用NewClassName")))typedef NewClassName OldClassName; // 兼容旧代码
  • 对性能有一定的影响。比如类成员本来可以用的对象的,必须用指针,不可避免的带来动态内存分配开销。

 

前向声明在有些时候还是不可避免的,比如两个类相互引用时。

 

这个改变,除了正确性问题外,还估计跟其分布式编译越来越快有关。

 

这个问题上,我个人的实践是

  • 一个项目内,可以使用前向声明,跨项目的别人的库,就要避免前向声明,直接包含头文件。
  • 值类型,特别是在运行时构造很频繁类型,不采用前向声明,比较重而复杂的类,需要对外屏蔽实现细节时,才考虑使用前向声明。
  • 使用前向声明隐藏实现时,用pimpl方式,比各个成员都用性能上会好一些。
// 传统方式:// my_class.h// 两次内存分配,引入了两个不完整类型Foo, Bar。class Foo;class Bar;class MyClass { public:  MyClass(); private:  Foo* foo_;  Bar* bar_;};// ===================================// pimpl方式// 一次内存分配,不引入任何无关符号。// my_class.hclass MyClass { public:  MyClass(); private:  struct Impl;  std::unique_ptr
impl_;};// my_class.cc#include "foo.h"#include "bar.h"struct MyClass::Impl { Foo foo; Bar bar;};MyClass::MyClass() : impl_(new Impl) {}

-inl.h

旧的规范中,当定义复杂的inline函数或者函数模版时,鼓励把这部分代码从头文件中提取出来,放到单独的filename-inl.h中。这个实践在过去很常见,现在不允许了。

嵌套类

旧规范中禁止,新规范中取消了,估计跟不再鼓励前向声明有关,因为嵌套类不能前向声明。

嵌套类可以使接口定义层次化,减少不必要的关注点。

函数重载

旧规范中不鼓励函数重载,要求函数行为相同时才允许重载,新规范中有所放宽,只要读代码时能看比较容易看出调了那个函数,就允许重载。

这个要求依然比较保守,毕竟构造函数天然都是重载的。

默认参数

旧规范中,禁止使用默认参数。新规范中,除了虚函数外,允许使用默认参数,何时使用的决策原则和函数重载的原则一样。

运算符重载

旧规范中,禁止重载运算符;新规范中,改为了“审慎地”重载运算符。

运算符重载是C++中比较有特色的部分,一棒子打死显然是过于保守的。

指针和引用的选择

当一个常量可以是引用也可以是指针时,如何选择,旧规范中提,新规范中作了规定,这些情况下用指针更合适:

  • 当参数可以是null时
  • 当参数在函数内会被保存下来以后用时

在旧规范中,禁止使用流(iostream),说法是保持一致,只用FILE/printf。新规范中允许了:“恰当地”使用流,保持简单的方式使用。

所谓简单的方式使用,就是涉及复杂格式控制时最好不要用,因为不但代码更啰嗦,还会改变流的状态。

枚举值命名

最早的规范规定枚举值采用全大些下划线分割的方式,2009年以后改为了k开头,跟大小写混合的方式,以和宏名做区分。

关于C++11的新条款

auto类型

auto使代码更清晰时,局部变量鼓励使用auto,比如迭代器:

// C++03 Stylefor (std::map
::iterator i = m.begin(); i != m.end(); ++i) { std::cout << i->second;}// C++11 Stylefor (auto i = m.begin(); i != m.end(); ++i) { std::cout << i->second;}

 

尤其是当访问map时,特别推荐用auto:

for (const auto& item : some_map) {  const KeyType& key = item.first;  const ValType& value = item.second;  // The rest of the loop can now just refer to key and value,  // a reader can see the types in question, and we've avoided  // the too-common case of extra copies in this iteration.}

 因为很多人可能不知道map的value_type是std::pair<const KeyType, MappedType>而不是std::pair<KeyType, MappedType>,但是当你用后者时,编译是能通过的,因为存在隐式构造类型转换。但是转换后的对象就不再是map中存的那个。

新的函数定义语法

C++11引入了一种新的函数定义语法

auto foo() -> int {  return 0;}

规范规定,只有必须使用这种语法才行时,才能用,常规情况下还是要使用普通的方式。

template 
<这里填什么类型合适呢??>
add(T t, U u) { return t + u;} // 新语法解决了这个问题 template
auto add(T t, U u) -> decltype(t+u) {
return t + u; }

右值引用

右值引用允许用于移动构造函数和移动赋值函数以及完美转发。

C++03中,对象的拷贝构造函数可能是个很大的开销,代码风格不鼓励返回复杂对象。比如

std::vector
foo() { std::vector
v; ... return v;}std::vector
v = foo();

尽管编译器普遍支持(匿名和命名的)返回值优化,但是还是有很多时候这种拷贝不可消除。C++11引入了右值引用,函数重载时,临时对象优先匹配绑右值引用的版本,这样的函数知道其参数是临时对象,就可以把其资源直接“移动”过来,避免拷贝。

C++标准库组件比如string和stl容器,大范围支持了基于右值引用的移动构造和赋值,即使你自己的代码没有对右值引用做任何处理,很多涉及这些对象拷贝和赋值的场景也自动得到了优化。

如果掌握了右值引用,这条规范就允许你针对自定义类做移动构造和赋值优化,从而进一步提高代码性能。

 

大括号初始化语法

鼓励使用,能简化代码

auto p = new vector
{"foo", "bar"};// A map can take a list of pairs. Nested braced-init-lists work.map
m = {
{1, "one"}, {2, "2"}};// A braced-init-list can be implicitly converted to a return type.vector
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
v) {}TestFunction2({1, 2, 3});A user-defined type can also define a constructor and/or assignment operator that take std::initializer_list
, which is automatically created from braced-init-list:

 

constexpr

鼓励使用

在C++中,const关键字实际有两种含义:

const int N = 100;    // 编译期间常量,可以做数组纬度,可以做模板非类型参数。const int N = rand(); // 运行期常量,不能进行上述用途,只能保证不能被修改。int a[N];             // 第一种定义OK,第二种编译出错。

C++11中,引入了constexpr关键字,用来定义“真正”的常量,可以确保是编译期间就能确定的。

在C++03中,const能用于函数,但是返回的不是编译期常量。

const int size() {  return 1000;}const int Size = size(); // Size不是编译期常量

constexpr不但可以用于常量,还能用于函数。

constexpr int size() {  return 1000;}const int Size = size(); // Size是编译期常量int a[Size]; // OK

 

nullptr

鼓励使用nullptr代替NULL

好处:有类型,可重载。

这段代码在C++03中会引发编译错误,因为NULL实际定义为0(gcc的NULL定义为(__null),不影响这里的行为)。

void f(int);void f(void*);f(NULL);

C++11中,用nullptr就没有歧义

f(nullptr); // 调 f(void*)

由于nullptr是有类型的,在某些情况下用于重载:

template 

 

 

sizeof

旧规范中说,尽量用sizeof(变量名)而不是sizeof(类型名),因为这样当类型改变时,可以避免改动多处。新规范对此作了进一步的完善,规定当代码跟具体的某个变量无关的场合时,还是要用sizeof(类型的):

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

 

lambda表达式

恰当使用lambda表达式。lambda在结构复杂的代码中,可以减少回调函数的定义,使STL中基于谓词的各种算法(foreach, find_if等)真正好用起来。

 

override关键字

鼓励使用

用C++的人都遇到过这种情况,基类定义了一个虚函数,我们在派生类中覆盖了这个虚函数,结果由于失误,签名没弄对

class Shape { public:  virtual void Rotate(double radians) = 0;};class Circle : public Shape { public:  virtual void Rotate(float radians);    // 错误情况1:类型搞错了  virtual void Rotete(double radians);   // 错误情况2:函数名写错了};

这两种错误情况都会导致虚函数没有真正被覆盖,运行时才能发现。

针对第一种情况,gcc有个编译警告选项,-Woverride-virtual,能发现大多数错误。

第二中情况就麻烦了,毕竟编译器不会替我们做拼写检查,一种不完善的方案是,在基类中把虚函数声明成纯虚函数,但是不是所有的场合都适合把虚函数定义为纯虚函数。

C++11引入了override关键字,来解决这个问题:

class Circle : public Shape { public:  void Rotate(double radians) override;};

override关键字表明这个函数是覆盖基类中同签名的函数,如果基类中不存在同签名的函数,编译期间就会报错。

总结

GCSG对C++11的新特性做了不少有价值的分析,完善了新的规范,另外随着C++语言新风格的普及,保守程度也下降了不少。

整体看来,GCSG是一个成熟,比较靠谱,容易实施,与时俱进的代码规范,还是很实用的。

你可能感兴趣的文章
Node.js之Stream双工流
查看>>
用迁云工具自建遗留系统镜像
查看>>
python爬虫js加密解密系列文章合集
查看>>
河南申请高新技术企业需要多少个专利?智为知识产权为您解答! ...
查看>>
怎样让GPS定位系统应用开发变得简单,选择底子很关键
查看>>
dubbo2.5-spring4-mybastis3.2-springmvc4-mongodb3.4-redis3.2整合(五)Spring中spring-data-redis的使用...
查看>>
局部区块多个报表 TAB 页切换及局部区块的参数查询
查看>>
「镁客早报」FDA试点项目将测试区块链,以遏制假药;努比亚折叠屏新品将亮相MWC2019...
查看>>
IDEA 插件开发入门教程
查看>>
纪念Galaxy系列10周年,三星推出了价值13310元的折叠屏手机
查看>>
体验云上快速搭建WordPress网站小记
查看>>
设计模式——代理模式
查看>>
加强市场拓展技术研发,企鹅科技获蚂蚁金服战略投资
查看>>
Java实现Redis发布订阅
查看>>
为云下IDC赋能-组建多可用区多地域的混合云(专线)最佳实践
查看>>
基于hi-nginx的web开发(python篇)——起步
查看>>
html5知识点补充—GeoLocation API位置感知
查看>>
kubernetes+docker监控之简介
查看>>
What is base..ctor(); in C#?
查看>>
互联网之父:互联网有两个与生俱来的问题
查看>>