[Effective Modern C++(11&14)]Chapter 3: Moving to Modern C++

1. Distinguish between () and {} when creating objects

  • C++11中,初始化值的指定方式有三种:括号初始化,等号初始化和花括号初始化;其中花括号初始化是为了解决C++98的表达能力而引入的一种统一初始化思想的实例。

    • 等号初始化和花括号初始化可以用于非静态成员变量的初始化

      class Widget {
        ...
        private:
           int x {0}; // ok
           int y = 0; // ok
           int z(0); // error
      };
    • 括号初始化和花括号初始化可以用于不可拷贝对象的初始化

      std::atomic<int> ai1 {0}; // ok
      std::atomic<int> ai2 (0); //ok
      std::atomic<int> ai3 = 0; // error
    • 花括号初始化会禁止窄化转型,而等号初始化和括号初始化会自动窄化转型

      double x, y, z;
      ...
      int sum1 {x+y+z}; // error
      int sum2 (x+y+z); // ok
      int sum3 = x+y+z; // ok
    • 调用对象的无参构造函数时,使用括号初始化会被编译器错误识别为声明了一个函数,而花括号初始化则能正确匹配到无参构造函数的调用

      Widget w1(); // error
      Widget w2{}; // ok 
    • 花括号初始化与std::initializer_lists和构造函数重载解析的同时出现时容易造成错误调用
      • 在调用构造函数的时候,只要不涉及到std::initializer_list参数,括号和花括号初始化有相同的含义

        class Widget {
            public:
                Widget(int i, bool b);
                Widget(int i, double d);
                ...
        };
        
        Widget w1(10, true); // calling 1
        Widget w2{10, true}; // calling 2
        Widget w3(10, 5.0);  // calling 1
        Widget w4{10, 5.0};  // calling 2
      • 如果涉及到std::initializer_list参数,在使用花括号初始化时,编译器会强烈地偏向于调用使用std::initializer_list参数的重载构造函数

        class Widget {
            public:
                Widget(int i, bool b);
                Widget(int i, double d);
                Widget(std::initializer_list<long double> il);
                ...
        };
        
        Widget w1(10, true); // calling 1
        Widget w2{10, true}; // calling 3, 10 and true convert to long double
        Widget w3(10, 5.0);  // calling 1
        Widget w4{10, 5.0};  // calling 3 , 10 and 5.0 convert to long double
        • 甚至本来应该调用拷贝构造函数或者移动构造函数,也会被std::initializer_list构造函数给劫持

          Widget w5(w4); // copy construction
          Widget w6{w4}; // std::initializer_list construction
          Widget w7(std::move(w4)); // move construction
          Widget w8{std::move(w4)}; // std::initializer_list construction
        • 编译器非常偏向选择std::initializer_list构造函数,以至于即便最匹配的std::initializer_list构造函数不能被调用,编译器也会优先选择它

          class Widget {
             public:
                 Widget(int i, bool b);
                 Widget(int i, double d);
                 Widget(std::initializer_list<bool> il);
                 ...
          };
          
          Widget w{10, 5.0}; // error, requires narrowing conversions
        • 只有当没有办法在花括号初始化的参数类型和std::initializer_list的参数类型之间进行转换时,编译器才会重新选择正常的构造函数

          class Widget {
             public:
                 Widget(int i, bool b);
                 Widget(int i, double d);
                 Widget(std::initializer_list<std::string> il);
                 ...
          };
          
          Widget w1(10, true); // calling 1
          Widget w2{10, true}; // calling 1
          Widget w3(10, 5.0);  // calling 2
          Widget w4{10, 5.0};  // calling 2
        • 当类同时支持默认构造函数和std::initializer_list构造函数时,此时调用空的花括号初始化,编译器会解析为调用默认构造函数,而要解析成std::initializer_list构造函数,需要在花括号中嵌套一个空的花括号进行初始化

          class Widget {
              public:
                  Widget();
                  Widget(std::initializer_list<int> il);
                  ...
          };
          
          Widget w1; // calling 1
          Widget w2{}; // calling 1
          Widget w3{{}}; // calling 2
          Widget w4({}); // calling 2

2. Prefer nullptr to 0 and NULL

  • C++会在需要指针的地方把0解释成指针,但是需要策略还是把0解释成int
  • C++98中上面这种做法会使得在指针和int型重载共存时产生意外匹配调用

    void f(int);
    void f(bool);
    void f(void*);
    f(0);  // calls f(int)
    f(NULL); // might not compile, but typically calls f(int)
  • nullptr的优点在于它没有一个整型类型,也没有一个指针类型,但是可以代表所有类型的指针,nullptr的实际类型是nullptr_t,可以被隐式地转换成所有原始指针类型
    f(nullptr); // calls f(void*)
  • 当在使用模板时,nullptr的优势就发挥出来了,可以转换成任意指针类型
    int f1(std::shared_ptr<Widget> spw);
    int f2(std::unique_ptr<Widget> upw);
    bool f3(Widget* pw);
    std::mutex f1m, f2m, f3m;
    
    template<typename FuncType, typename MuxType, typename PtrType>
    auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))
    {
         using MuxGuard = std::lock_guard<MuxType>;
         MuxGuard g(mutex);
         return func(ptr);
    }
    
    auto result1 = lockAndCall(f1, f1m, 0); // error, PtrType is int
    auto result2 = lockAndCall(f2, f2m, NULL); // error, PtrType is int / long
    auto result3 = lockAndCall(f3, f3m, nullptr); // ok 

3. Prefer alias declarations to typedefs

  • aliastypedef更容易理解

    typedef void (*FP)(int, const std::string&);
    using FP = void(*)(int, const std::string&);
  • alias可以模板化,而typedef不能直接模板化,需要借助结构体来实现
    • 如果要定义一个使用自定义分配器的链表

      template<typename T>
      using MyAllocList = std::list<T, MyAlloc<T>>;
      MyAllocList<Widget> lw;
      
      template<typename T>
      struct MyAllocList {
         typedef std::list<T, MyAlloc<T>> type;
      };
      MyAllocList<Widget>::type lw;
    • 如果要在模板内部创建一个持有模板参数类型的链表,必须在typedef名字前面加上typename
      template<typename T>
      class Widget {
          private:
              typename MyAllocList<T>::type list;
              ...
      };
      • MyAllocList<T>::type指的是一个取决于模板类型参数T的类型,因此就是一个依赖类型,C++规定依赖类型前面必须加上typename
      • 如果使用alias定义模板,就不需要typename
        template<typename T>
        using MyAllocList = std::list<T, MyAlloc<T>>; 
        
        template<typename T>
        class Widget {
           private:
               MyAllocList<T> list;
               ...
        };
        • 此处看起来MyAllocList<T>是一个与模板参数T存在依赖关系的对象,但是当编译器处理Widget模板时,它知道MyAllocList<T>是一个类型的名字,因为MyAllocList是一个别名模板:它必须命名一个类型,因此MyAllocList<T>是一个无依赖类型,也就不需要typename
        • typedef中,当编译器在Widget模板中看到MyAllocList<T>::type时,它们不能确定这是否是一个类型,因为有可能是MyAllocList<T>的一个特例而它们没看到,例如:
          class Wine{...};
          
          template<>
          class MyAllocList<Wine> {
              private:
                  enum class WineType {White, Red, Rose};
                  WineType type;  //!!!!!!!!!!!!!!!
                  ...
          };
  • C++11以类型萃取的形式提供了许多形式转换工具,模板都在<type_traits>头文件中,例如
    std::remove_const<T>::type
    std::remove_reference<T>::type
    std::add_lvalue_reference<T>::type
    • 但是要在模板内部使用它们时,仍然要在前面加上typename,因为它们实际上还是用嵌套typedef实现的
    • 但在C++14中,它们有了替代的方案
      std::remove_const_t<T>
      std::remove_reference_t<T>
      std::add_lvalue_reference_t<T>
      • 原理显而易见

        template<class T>
        using remove_const_t = typename remove_const<T>::type;
        
        template<class T>
        using remove_reference_t = typename remove_reference<T>::type;
        
        template<class T>
        using add_lvalue_reference_t = typename add_lvalue_reference<T>::type;

4. Prefer scoped enums to unscoped enums

  • 通常情况下,在花括号内声明一个名字可以限制名字对外的可见性,但是对于C++98enums中的enumerators并非如此,其对外也是可见的

    enum Color {black, white, red};
    auto while = false; // error, while already declared in this scope
  • C++11的新标准,有范围限制的enums,并不会对命名空间造成污染

    enum class Color {black, white, red};
    auto white = false; // fine
    Color c = white; // error, no enumerator named “white" is in this scope
    Color c = Color::white; // fine
    auto c = Color::white; // fine
  • 有范围限制enums中的枚举常量有更强的类型,而对于无范围限制的enums中枚举常量会被隐式转换成整型类型

    enum Color {black, white, red};
    std::vector<std::size_t> primeFactors(std::size_t x);
    
    Color c = red;
    ...
    if( c < 14.5){ // compare Color to double!!
       auto factors = primeFactors(c); // compute prime factors of a Color!!
       ...
    }
    
    enum class Color {black, white, red};
    Color c = Color::red;
    ...
    if( c < 14.5){ // error, can‘t compare Color and double!!!
        auto factors = primeFactors(c); // error, can‘t pass Color to function expecting std::size_t
        ...
    }
  • 如果要把C++11中的enums变量转换成其他类型,需要使用static_cast<>()

    if( static_cast<double>(c) < 14.5 ){ // valid
        auto factors = primeFactors(static_cast<std::size_t>(c)); // valid
        ...
    }
  • C++中每个enum都有一个由编译器决定的整型底层类型,为了有效利用内存,编译器通常会选择足够代表枚举量范围的最小的底层类型,为此,C++98只支持enum定义(列出所有的枚举值),而不支持声明,这使得在使用enum前,编译器能选择一个底层类型。
  • 无法对enum前置声明有许多缺点,最显著的就是增加编译的依赖性,如果一个enum被系统中每个组件都有可能用到,那么都得包含这个enum所在的头文件,如果需要新加入一个枚举值,整个系统就有可能重新编译,即便只有一个函数使用这个新的值
  • C++11中的enum类可以消除这个编译需求,例如

    #file 1
    enum Status {
       good = 0,
       failed = 1,
       incomplete = 100,
       corrupt = 200,
       audited = 500,
       indeterminate = 0xFFFFFFFF
    };
    
    #file 2
    enum class Status;
    void continueProcessing(Status s);
    • 如果修改了Status,而且continueProcessing没有使用到新的值,那么file2就不需要重新编译
    • 但是如果编译器在使用一个enum之前,需要知道它的大小该怎么办?
      • 对于一个有范围限制的enum,它的底层类型是已知的(默认是int,可以手动覆盖),而对于没有范围限制的enum,底层类型可以指定

        enum class Status; //int, declaration
        enum class Status: std::uint32_t; //uin32_t, declaration
        enum Color: std::uint8_t;// uint8_t, declaration
        
        enum class Status: std::uint32_t {
            good = 0,
            failed = 1,
            incomplete = 100,
            corrupt = 200,
            audited = 500,
            indeterminate = 0xFFFFFFFF
        };
  • 无范围限制的enumC++11std::tuples中的用途
    using UserInfo = std::tuple<std::string, std::string, std::size_t>; // name, email, reputation
    UserInfo uInfo;
    ...
    auto val = std::get<1>(uInfo); // get value of field 1, but can you always remember what the hell 1 represents?
    
    //Improve
    enum UserInfoFields {uiName, uiEmail, uiReputation};
    UserInfo uInfo;
    ...
    auto val = std::get<uiEmail>(uInfo); // implicit conversion from UserInfoFields to std::size_t, which is the type that std::get requires
    • 如果要改写成有范围限制的enum,略显拖沓

      enum class UserInfoFields {uiName, uiEmail, uiReputation};
      UserInfo uInfo;
      ...
      auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);

5. Prefer deleted functions to private undefined ones

  • 删除的函数和声明为private的函数之间的区别

    • 删除的函数在任何地方都不能使用,所以成员函数和友元函数都不能使用已经删除的函数,否则会编译失败,这在C++98中会推迟到链接阶段才会报错
    • 删除的函数是pulic而不是private,因为当客户端代码试图使用这个删除的成员函数时,C++会首先检查访问权限,后检查删除状态,如果设为private,编译器给出的是权限不足警告而不是函数不可用警告
    • 任何函数都可以是deleted状态,而只有成员函数可以是private,例如删除某些过时的重载函数

      bool isLucky(int number);
      bool isLucky(char) = delete;
      bool isLucky(bool) = delete;
      bool isLucky(double) = delete;
    • 虽然删除的函数不能使用,但仍然是程序的一部分,因此,在重载解析过程中也会被纳入考虑中
    • 模板函数可以通过删除来阻止部分实例化函数,而允许其他实例化存在

      template<typename T>
      void processPointer(T* ptr);
      
      template<>
      void processPointer<void>(void*) = delete;
      
      template<>
      void processPointer<char>(char*) = delete;
    • 有意思的是,如果在类里面有一个模板函数,则不能通过设置private来禁用一些实例化,因为不能给一个成员函数的模板特化一个不同于主模板的访问权限,例如
      class Widget {
          public:
              ...
              template<typename T>
              void processPointer(T* ptr) {...}
          private:
              template<>
              void processPointer<void>(void*); // error
      };
      • 问题在于模板特化必须被卸载命名空间范围内,而不是在类范围内,因此可以使用delete来实现

        class Widget {
            public:
                ...
                template<typename T>
                void processPointer(T* ptr) {...}
                ...
        };
        
        template<>
        void Widget::processPointer<void>(void*) = delete;

6. Declare overriding functions override

  • 覆盖产生的必要条件

    • 基类函数必须是virtual
    • 基类和派生类的函数名必须一致
    • 基类和派生类函数的参数类型必须一致
    • 基类和派生类函数的const属性必须一致
    • 基类和派生类函数的返回类型以及异常说明必须兼容
    • 函数的引用修饰必须一致(C++11)
      • 限制成员函数的使用只能是左值或者右值(*this)

        class Widget {
           public:
           ...
           void doWork() &; // only when *this is an lvalue
           void doWork() &&; // only when *this is an rvalue
        };
        
        ...
        Widget makeWidget();
        Widget w;
        ...
        w.doWork();
        makeWidget().doWork();
  • 显式地对成员函数声明override能使得编译器检查是否正确覆盖,而不是在没有正确覆盖时隐式地转换成了重载或者其他合法函数,而使得调用时发生意外调用,例如
    class Base{
       public:
           virtual void mf1() const;
           virtual void mf2(int x);
           virtual void mf3() &;
           void mf4() const;
    };
    
    class Derived: public Base {
       public:
           virtual void mf1();  // not const
           virtual void mf2(unsigned int x);  // not int
           virtual void mf3() &&; // not &
           void mf4() const; // not virtual in base
    };
    • 虽然上面的函数都没有发生覆盖,但是有些编译器认为都是合法的,而不会给出警告,正确的做法是

      class Derived: public Base {
          public:
              virtual void mf1() override;
              virtual void mf2(unsigned int x) override;
              virtual void mf3() && override;
              virtual void mf4() const override;
      };
      • 此时,编译器能检查出所有的错误覆盖

7. Prefer const_iterators to iterators

8. Declare functions noexcept if they won‘t emit exceptions

9. Use constexpr whenever possible

  • 对于constexpr对象,它们具有const属性,并且它们的值在编译的时候确定(从技术角度讲,是在转换期间确定,转换期包括编译和链接),它们的值也许会被放在只读内存区中,它们的值也能被用在整型常量表达式中,例如数组长度,整型模板参数,枚举值,对齐指示符等等
  • constexpr函数使用constexpr对象时,它们会产生编译期常量,如果constexpr函数使用了运行时的值,它们就会产生运行时的值,但是如果constexpr函数使用的所有参数都是运行时的值,那么就会报错
  • C++11中,constexpr函数只能包含不超过一条return语句的执行语句,但是可以使用条件运算符和递归来实现多重运算。
  • C++14中,constexpr函数的语句数量没有限制,但是函数必须接收和返回字面值类型,也就是指可以在编译期间确定值的类型。
  • 字面值类型包括除了void修饰的类型和带有constexpr修饰的用户自定义类型(因为构造函数和其他成员函数也可能是constexpr)

    class Point {
       public:
           constexpr Point(double xVal = 0, double yVal = 0) noexcept: x(xVal), y(yVal) {}
           constexpr double xValue() const noexcept { return x;}
           constexpr double yValue() const noexcept { return y;}
           void setX(double newX) noexcept { x = newX;}
           void setY(double newY) noexcept { y = newY;}
       private:
           double x, y;
    };
    constexpr Point p1(9.4, 2.7);
    constexpr Point p2(28.8, 5.3);
    
    constexpr Point midpoint(const Point& p1, const Point& p2) noexcept
    {
         return { (p1.xValue() + p2.xValue()) / 2, (p1.yValue() + p2.yValue()) / 2 };
    }
    
    constexpr auto mid = midpoint(p1, p2);
  • C++11中,setXsetY不能被声明为constexpr,因为不能在const成员函数中修改成员变量,而且返回值为void,并不是字面值常量,但是C++14中是允许的

10. Make const member functions thread safe

11. Understand special member function generation

  • 特殊成员函数是C++会自动生成的函数,C++98中有四个这样的函数:默认构造函数,析构函数,拷贝构造函数,拷贝赋值运算符;C++11中多了两个:移动构造函数和移动赋值运算符
  • 两个拷贝操作是无关的,声明一个不会阻止编译器产生另一个
  • 两个移动操作是相关的,声明一个会阻止编译器自动产生另一个
  • 显式声明一个拷贝操作后,移动操作就不会被自动生成,反之依然,理由是:比如声明了拷贝运算,就说明移动操作不适合用于此类
  • 三条规则:如果声明了拷贝构造,拷贝赋值或者析构函数中任何一个,都应该将三个一起声明,因为这三个函数是相互关联的
  • 三条规则暗示了析构函数的出现使得简单的memberwise拷贝不适合类的拷贝操作,也就是说如果声明了析构函数,那么就不应该自动生成拷贝操作相关的函数,因为可能会存在不一致的资源管理行为。同样的,也不应该自动生成移动操作相关的函数。所以,只有当类满足下面三个条件时,移动操作才会自动生成:
    • 没有声明拷贝操作
    • 没有声明移动操作
    • 没有声明析构函数
  • 假如编译器生成的函数行为正确,那么我们只需要在函数名后面加上default就可以了,然编译器接管一切具体事务。  

原文地址:https://www.cnblogs.com/burningTheStar/p/8975712.html

时间: 2024-11-05 22:49:19

[Effective Modern C++(11&14)]Chapter 3: Moving to Modern C++的相关文章

[Effective Modern C++(11&amp;14)]Chapter 2: auto

1.更多的使用auto而不是显式类型声明 将大段声明缩减成auto 例如: typename std::iterator_traits<It>::value_type currValue = *b; auto currValue = *b; 使用auto可以防止变量未初始化 例如: int x1; //正确,但是未初始化 auto x2; //错误,没有初始化 auto x3 = 3; //正确,声明并初始化 在模板函数中可以使用auto来完成变量的自动类型推导 例如: template<

《modern operating system》 chapter 3 MEMORY MANAGEMENT 笔记

MEMORY MANAGEMENT The part of the operating system that manages (part of) the memory hierarchy is called thememory manager 这章感觉有点多...80 多页..看完都看了两天多,做笔记就更有点不想...有点懒了..但是要坚持下去,可以自己较劲 对于内存的抽象,最简单的抽象就是...没有抽象 和第一次看不一样,把summary放在最前面,对整个mamory management的

11.14 Daily Scrum

通过一天的努力,大家的任务基本已经完成,主界面的功能也日趋完善,后续的数据库处理和软件搜索等工作也已经开始陆续开展,到目前一共出现了三个问题急待解决,一是我们的燃尽图工作开展较晚,导致了大量已经展开的工作出现无法记录的情况,二是数据库开展的方面出现了不少的问题,由于我们才刚刚开始学习数据库的基本知识,有许多问题还没有得到解决,我们的方案是通过实例学习掌握数据库的基本技能.三是工作难题越来越多,工作量越来越大,有很多问题悬而不决,只有通过团队的合作意识的不断深入,分工的具体化的不断加深,这些问题就

Effective C++:条款14:在资源管理类中小copying行为

(一) 上一条款说的auto_ptr和tr1::share_ptr适合于heap-based的资源,然而并不是所有资源都是heap-based的.换句话说并不是tr1::shared_ptr 和 auto_ptr 永远适合做为资源的管理者.所以有时难免还是需要实现自己的资源管理类型. 假设Mutex类型通过lock和unlock两组函数进行互斥器的锁定和解锁,可能我们希望和auto_ptr一样的行为,在某个智能类型析构时主动调用unlock进行解锁.比如下面的代码: void lock(Mute

Effective C++ 条款11,12 在operator= 中处理&ldquo;自我赋值&rdquo; || 复制对象时不要忘记每一个成分

1.潜在的自我赋值     a[i] = a[j];     *px = *py; 当两个对象来自同一个继承体系时,他们甚至不需要声明为相同类型就可能造成别名. 现在担心的问题是:假如指向同一个对象,当其中一个对象被删,另一个也被删,这会造成不想要的结果. 该怎么办? 比如:   widget& widget:: operator+ (const widget& rhs) {    delete pd;    pd = new bitmap(*rhs.pb);    return *thi

Effective C++ Item 11 在operator= 中处理“自我赋值”

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 经验:确保当对象自我赋值时operator=有良好行为.其中技术包括比较"来源对象"和"目标对象"的地址.精心周到的语句顺序.以及copy-and-swap. 示例:没有"证同测试" #include <iostream> #include <string> using namespace std; class Bi

Effective C++ 条款13/14 以对象管理资源 || 在资源管理类中小心拷贝行为

三.资源管理       资源就是一旦你使用了它,将来不用的时候必须归还系统.C++中最常用的资源就是动态内存分配.其实,资源还有 文件描述符.互斥器.图形界面中的字形.画刷.数据库连接.socket等. 1.        以对象管理资源       void f() {     investment *plv = createInvestment();     //这里存在很多不定因素,可能造成下面语句无法执行,这就存在资源泄露的可能.     delete plv; }      这里我们

算法导论之十(十一章散列表11.1-4大数组实现直接寻址方式的字典操作)

11.1-4题目: 我们希望在一个非常大的数组上,通过利用直接寻址的方式来实现一个字典.开始时,该数组中可能包含一些无用信息,但要对整个数组进行初始化是不太实际的,因为该数组的规模太大.请给出在大数组上实现直接寻址字典的方式.每个存储对象占用O(1)空间:SEARCH.INSEART.DELETE操作的时间均为O(1):并且对数据结构初始化的时间为O(1).(提示:可以利用一个附加数组,处理方式类似于栈,其大小等于实际存储在字典中的关键字数目,以帮助确定大数组中某个给定的项是否有效). 想法:

测试管理工具实践(小组作业)——11.14

今日工作进度情况: 李璋毅:查阅资料后撰写了Testlink工具的名称,优缺点和主要功能,发布博客: 储志峰:搭建安装Testlink之前所需要的环境:Webserver,PHP4,Mysql: 刘伟清:完成了VetrigoServ和Textlink的安装,并了解了Textlink的基本结构 翟瑆: 安装Testlink,并收集关于Testlink的使用方法: 陈汉:下载Vertrigoserv2.43以搭建环境,Testlink1.9.14的下载.安装与配置,使用Testlink创建测试用例: