zoukankan      html  css  js  c++  java
  • Google C++ Style Guide在C++11普及后的变化

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

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

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

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

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

    扯得有点远了……

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

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

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

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

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

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

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

    旧条款的更新

    前向声明

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

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

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

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

        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.h
    class MyClass {
     public:
      MyClass();
     private:
      struct Impl;
      std::unique_ptr<Impl> 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 Style
    for (std::map<int, std::string>::iterator i = m.begin(); i != m.end(); ++i) {
      std::cout << i->second;
    }
    
    // C++11 Style
    for (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 <typename T, typename U>
    <这里填什么类型合适呢??> add(T t, U u) { return t + u; }

    // 新语法解决了这个问题
    template <typename T, typename U>
    auto add(T t, U u) -> decltype(t+u) {
    return t + u;
    }

    右值引用

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

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

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

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

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

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

    大括号初始化语法

    鼓励使用,能简化代码

    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});
    A user-defined type can also define a constructor and/or assignment operator that take std::initializer_list<T>, 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 <template T>
    class shared_ptr {
     public:
      constexpr shared_ptr(std::nullptr_t);
      explicit shared_ptr(T*);
    };
    
    constexpr shared_ptr<int> EMPTY(nullptr);

    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是一个成熟,比较靠谱,容易实施,与时俱进的代码规范,还是很实用的。

  • 相关阅读:
    ovs tag
    从数据库分析OpenStack创建虚机流程
    Neutron中的二层网络服务架构
    Failed to bind port
    OpenStack网络参数segment
    OpenStack与SDN控制器的集成
    HDU 3709 Balanced Number
    HDU 5787 K-wolf Number
    HDU 5803 Zhu’s Math Problem
    CodeForces 258B Little Elephant and Elections
  • 原文地址:https://www.cnblogs.com/chen3feng/p/5972967.html
Copyright © 2011-2022 走看看