• 【读书笔记】Effective Modern Cpp(一)


    这段时间看完了这本书。。做了些书中的笔记。。

    我只是选了自己理解或者觉得可能重要的部分。。

    对我来说后面同步异步那里确实有点看不懂。。

    建议书里代码跟着写写,会明白一点。

    类型推断

    01 模板类型推断机制

    • auto 推断的基础是模板类型推断机制
    //模板形式
    template<typename T>
    void f(ParamType x);
    //调用
    f(expr)
    
    //编译期间编译器用expr推断T和Paramter
    void f(const T& x);
    int x;
    f(x); //T被推断成int,ParamType推断成const int&
    

    所以T的类型推断是与exprParamType有关

    • 以下情况不适用
      • ParamType 不是引用或指针
      • ParamType 是引用类型
      • ParamType 是指针类型
      • ParamType 是转发引用
      • expr 是函数名

    02 auto

    • auto类型推断和模板类型推断一致
    • 变量用auto声明时没,auto扮演了模板T的角色‘修饰符扮演ParamType
    • 模板的调用相当于对应的初始化表达式
    • auto的推断适用模板的三种情形:
    auto x = 1;				//int
    const auto cx = x;		//const int
    const auto& rx = x;		//const int&
    auto&& uref1 = x;			//int& 
    auto&& uref2 = cx;		//const int&
    auto&& uref3 = 1;			//int&& 
    
    • auto对数组和指针的推断也和模板一致
    • auto不同于模板实参推断的情形是c++11的初始化
    auto x1 = 1;			//int x1
    auto x2(1);			//int x2
    auto x3 = { 1 };		//std::initializer_list<int> x3
    auto x4{ 1 };			// C++11为std::initializer_list<int> x4,						// C++14为int x4
    
    • 初始化列表中元素不同,无法推断
    • c++14禁止对auto用std::initializer_list进行初始化,必须用 =
    • 模板不支持模板参数为 T 而 expr 为初始化列表的推断;不过将模板参数为 std::initializer_list]则可以推断 T
    auto x = {1,2,3};	//std::initializer_list<int>
    template<typename T>
    void f(T x);
    f({1 , 2 , 3});	//这样是不行的
    
    void f(std::initializer_list<T> initList);
    void f({11,23,9});	//这样可以,T是int
    
    
    • C++14中的auto
    auto f(){return 1;}	//	可以作为函数返回类型。
    
    //此时还是用的模板实参推断机制,所以不能返回列表
    auto newInitList(){return {1} ; }	//错误的
    
    //泛型lambda同理
    
    • C++17中的auto
      • 可以作为非模板参数

    03 decltype

    • decltype 会推断直觉预期的类型
    • decltype一般用来声明与参数类型相关的返回类型(取决于元素类型)
    • C++14 允许将返回类型声明为 decltype(auto)
    template<typename Container, typename Index>
    decltype(auto) f(Container& c, Index i){
        return c[i];
    }
    
    • decltype(auto) 也可以作为变量声明类型
    • 特殊情况
    //解引用->推断成引用
    int* p;	//decltype(*p)-> int&
    
    //赋值表达式产生引用,类型为左值的引用类型
    int a = 0; int b = 1;
    decltype(a=1) c = b;	//int&
    c = 3;
    cout<<a<<b<<c;	//033
    
    //表达式加上括号,会变成特殊左值表达式。(引用)
    int i;	//decltype((i))	-> int&
    
    //返回类型是decltype(auto)时,可能导致返回局部变量的引用
    

    04 查看推断类型的方法

    • 利用IDE
    • 利用报错信息
    • 使用type_ id,std:: type_info::name
      cout<<typeid(T).name()<<endl;
    • 使用Boost.TypeIndex可以得到精确类型

    auto

    05 用auto替代显式类型声明

    • auto声明的变量必须初始化
    • 名称非常长的类型(迭代器之类),用auto简化工作
    • lambda生成的闭包类型用auto推断
    • 不使用auto可以改成std::function,但是后者调用闭包更慢
    • auto避免简写类型存在的问题
    • 显式类型声明如果能让代码更清晰,就不用auto

    06 auto推断出非预期类型时,先强制转换出预期类型

    auto x = f()[0];	//不是bool,std::vector<bool>::reference
    bool x = f()[0];	//隐式转换
    //解决出代理类问题,做一次预期强制转换
    auto x = static_cast<bool>(f()[0]);
    

    转向现代C++

    07 创建对象时注意区分()和{}

    • 初始化值
    int a(0);
    int b = 0;
    int c[0};
    int d = {0};	//int d{0}
    
    • “=”可能是拷贝
    X a;		//默认构造
    X b = a;	//拷贝
    a = b; 	//拷贝
    
    • c++11引入了统一初始化(大括号初始化)
      • 禁止内置类型的隐式收缩转换
      • 不用担心解析
      • 缺陷:总是优先匹配参数类型为std::initializer_list

    08 用nullptr替代0和NULL

    • NULL本质是宏,void*
    // VS2017中的定义
    #ifndef NULL
        #ifdef __cplusplus
            #define NULL 0
        #else
            #define NULL ((void *)0)
        #endif
    #endif
    
    • 重载解析时,NULL不会优先匹配指针类型。但是nullptr可以转换任何原始指针类型。所以用nullptr会使代码意图更清晰。

    09 用using别名声明替代typedef

    • using别名声明比typedef可读性更好。
    • C++11引入了别名模板,只能使用using别名声明;引入了type traits;C++14为了简化生成值的type traits,还引入了变量模板

    10 用enum class替代enum

    • enum成员属于enum所在的作用域,因此作用域不能出现同名实例
    • C++11引入了限定作用域的枚举类型。enum class
    enum class X {a,b,c};
    int a = 1;	//可;但是enum就不可以
    X x = X::a;	//可
    X y = b;		//不可
    
    • enum class不会进行隐式转换
    • c++11以前enum class不允许前置声明
    • 使用enum更方便的场景只有一种,要隐式转换

    11 用 = delete替代private作用域来禁用函数

    • C++11 中可以直接将要删除的函数用 =delete 声明
    class A {
    public:
        A(const A&) = delete;
        A& operator(const A&) = delete;
    }; 
    
    • private 作用域中的函数还可以被成员和友元调用,而 =delete 是真正禁用了函数,无法通过任何方法调用
    • 任何函数都可以用 =delete 声明
    • =delete 还可以禁止模板对某个类型的实例化

    12 用override标记被重写的虚函数

    • 重写虚函数的要求
      • 基类中必须有此虚函数
      • 基类和派生类的函数名相同(析构函数除外)
      • 函数参数类型相同
      • const属性相同
      • 函数返回值和异常说明相同
      • 引用修饰符相同(c++11)
    • 为了保证正确性,C++11 提供了override来标记要重写的虚函数。
    class A {
    public:
        virtual void f1() const;
        virtual void f2(int x);
        virtual void f3() &;
        virtual void f4() const;
    };
    
    class B : public A {
    public:
        virtual void f1() const override;
        virtual void f2(int x) override;
        virtual void f3() & override;
        void f4() const override;
    }; 
    
    • c++11还提供了final,可以用来制定虚函数禁止被重写;还可以用来指定某个类禁止被继承

    13 用std::cbegin和std::cend获取const_iterator

    • 需要迭代器但不修改值时就应该使用 const_iterator(c++14)
    std::vector<int> v{ 2, 3 };
    auto it = std::find(std::cbegin(v), std::cend(v), 2); // C++14
    v.insert(it, 1); 
    
    • C++11没有,但是可以实现
    template<class C>
    auto cbegin(const C& c)->decltype(std::begin(c))
    {
        return std::begin(c); // c是const所以返回const_iterator
    }
    

    14 用noexcept标记不抛异常的函数

    • C++98中,必须指出一个函数可能抛出的所有异常类型。
    • C++11中,关心函数会不会抛出异常,要么可能抛出异常,要么绝对不异常
    • C++17移除C++98的exception specification
    • 函数是否要加上 noexcept 声明与接口设计相关,调用者可以查询函数的 noexcept 状态,查询结果将影响代码的异常安全性和执行效率。
    • noexcept可以让编译器生成更好的目标代码。
    • 灵活程度noexcept > throw()
    • 函数声明成noexcept的前提是:保证函数长期具有noexcept性质
    • wide contract: 没有前置条件;narrow contract 有前置条件。

    15 用constexpr表示编译期常量

    • constexpr 用于对象时,是加强版的const,在编译期已知。编译期已知的值可能被放进只读内存。
    • constexpr 调用时传入编译期常量,产出也是编译期常量。传入运行期才能知道的值,则产出运行期值。constexpr 满足所有需求。
    • C++11 中,constexpr 函数只能包含一条语句,即一条 return 语句。
    constexpr int pow(int base, int exp) noexcept
    {
        return (exp == 0 ? 1 : base * pow(base, exp - 1));
    } 
    

    但是C++14解除了这个限制

    • constexpr 函数必须传入和返回literal type;C++14允许对值进行了修改或者无返回值的函数声明成constexpr。
    • 使用 constexpr 的前提是必须长期保证需要它

    16 用std::mutex或std::atomic保证const成员函数线程安全

    • 假如此时有两个线程对同一个对象调用成员函数,虽然函数声明为 const,但由于函数内部修改了数据成员,就可能产生数据竞争。最简单的解决方法是引入一个std::mutex
    • 对一些简单的情况,使用原子变量 std :: atomic 可能开销更低(取决于机器及 std::mutex 的实现
    • 同步多个变量或内存区,还是使用std::mutex

    17 特殊成员函数的隐式合成与抑制机制

    • 移动构造函数和移动赋值运算符
    class A {
    public:
        A(A&& rhs); // 移动构造函数
        A& operator=(A&& rhs); // 移动赋值运算符
    }; 
    
    • 移动操作会在需要时生成
    • 移动操作并不确保真正移动,std::move用于移动对象。对支持移动操作的基类移动,对不可移动的类型执行拷贝
    • 两种拷贝操作时独立的;两种移动操作是不独立的;显式声明拷贝操作会组织自动生成移动操作,反之亦然。(声明=defalut不阻止)
    • 默认构造函数和析构函数的生成
      • 默认构造函数:只在类中不存在用户声明的构造函数时生成
      • 析构函数:
        • 和 C++98 基本相同,唯一的区别是默认为 noexcept
        • 只有基类的析构函数为虚函数,派生类的析构函数才为虚函数
      • 拷贝构造函数:
        • 仅当类中不存在用户声明的拷贝构造函数时生成
        • 如果声明了移动操作,则拷贝构造函数被删除
        • 如果声明了拷贝赋值运算符或析构函数,仍能生成拷贝构造函数,但这是被废弃的行为
      • 拷贝赋值运算符:
        • 仅当类中不存在用户声明的拷贝赋值运算符时生成
        • 如果声明了移动操作,则拷贝赋值运算符被删除
        • 如果声明了拷贝构造函数或析构函数,仍能生成拷贝赋值运算符,但这是被废弃的行为
      • 移动操作:仅当类中不存在任何用户声明的拷贝操作、移动操作、析构函数时生成

    智能指针

    18 用std::unique_ptr管理所有权唯一的资源

    • std::unique_ptr是智能指针的首选,默认和原始指针尺寸相同
    • 对资源拥有唯一所有权(move-obly),不允许拷贝,用作工厂函数的返回类型
    • std::unique_ptr析构默认通过delete完成。
    • 拓展成支持继承体系的工厂函数
    class A {
    public:
        virtual ~A() {} // 删除器对任何对象调用的是基类的析构函数,因此必须声明为虚函数
    };
    class B : public A {}; // 基类的析构函数为虚函数,则派生类的析构函数默认为虚函数
    class C : public A {};
    class D : public A {};
    
    auto makeA(int i)
    {
        auto f = [](A* p) { std::cout << "destroy
    "; delete p; };
        std::unique_ptr<A, decltype(f)> p(nullptr, f);
        if(i == 1) p.reset(new B);
        else if(i == 2) p.reset(new C);
        else p.reset(new D);
        return p;
    }
    
    • std::unique_ptr可以转成 std ::shared _ptr
    // std::make_unique的返回类型是std::unique_ptr
    std::shared_ptr<int> p = std::make_unique<int>(42); 
    
    • std::unique_ptr针对数组特供,operator[],但是不提供*和->
    std::unique_ptr<int[]> p(new int[3]{11, 22, 33});
    for(int i = 0; i < 3; ++i) std::cout << p[i];
    

    19 用std::shared_ptr管理所有权可共享的资源

    • std::shared_ptr内部有一个引用计数,存储被共享次数。所以尺寸是原始指针两倍
    • std::shared_ptr保证线程安全
    • std::shared_ptr默认析构方式也是delete
    • control block在创建第一个std::shared_ptr确定。创建时期:
      • 调用std::make_ptr时
      • std::unique_ptr构造 std ::shared _ptr时
      • 从原始指针构造std::shared_ptr时
    • 一个原始指针构造多个std::shared_ptr,创建多个control block有多个引用指针,但是指针变为0会出现多次析构的错误 std ::make _shared不会有这个问题。
    • 用类的this指针构造std::make_ shared,*this的所有权不会被共享。因此需要继承std ::enable_ shared_ from_this。
    class A : public std::enable_shared_from_this<A> {
    public:
        std::shared_ptr<A> f() { return shared_from_this(); }
    };
    
    auto p = std::make_shared<A>();
    auto q = p->f();
    std::cout << p.use_count() << q.use_count(); // 22 
    

    20 用std::weak_ptr观测std::shared_ptr的内部状态

    • std::weak_ptr不能解引用,不是独立的,是 std :: shared _ptr的扩充。主要是观察其内部状态。
    std::weak_ptr<int> w;
    
    void f(std::weak_ptr<int> w)
    {
        if (auto p = w.lock()) std::cout << *p;
        else std::cout << "can't get value";
    }
    
    int main()
    {
        {
            auto p = std::make_shared<int>(42);
            w = p;
            assert(p.use_count() == 1);
            assert(w.expired() == false);
            f(w); // 42
            auto q = w.lock();
            assert(p.use_count() == 2);
            assert(q.use_count() == 2);
        }
        f(w); // can't get value
        assert(w.expired() == true);
        assert(w.lock() == nullptr);
    }
    
    • std::weak_ptr解决循环引用问题

    21 用std::make_unique(std::make_shared)创建std::unique_ptr(std::shared_ptr)

    • C++14提供std::make_unique,c++11自己实现。

    • 这个函数不支持数组和自定义删除器。

    • 优先使用make函数的一个明显原因就是只需要写一次类型

    • make函数有两个限制

      • 无法定义删除器:使用自定义删除器且避免内存泄漏
        std::shared_ptr<A> p(new A, d); // 如果发生异常,删除器将析构new创建的对象
      • make 函数中的完美转发使用的是小括号初始化,在持有 std::vector类型时,设置初始化值不如大括号初始化方便。
        解决方法先构造一个std::initializer_list再传入
    • std::make_shared和std :: allocate _shared还有两个限制

      • 如果类重载了 operator new和 operator delete,其针对的内存尺寸一般为类的尺寸,而 std:: shared_ptr还要加上 control block 的尺寸,因此 std::make _shared 不适用重载了 operator new和 operator delete的类
      • std:: make_ shared使 std:: shared_ ptr 的 control block 和管理的对象在同一内存上分配(比用new构造智能指针在尺寸和速度上更优的原因),对象在引用计数为0时被析构,但其占用的内存直到 control block 被析构时才被释放,比如 std:: weak_ ptr会持续指向control block(为了检查引用计数以检查自身是否失效),control block 直到最后一个 std::shared _ ptr和 std ::weak_ptr被析构时才释放

    22 用std::unique_ptr实现pimpl手法必须在.cpp文件中提供析构函数定义

    • pimpl手法就是把数据成员提取到类中,用指向该类的指针替代原来的数据成员。因为数据成员会影响内存布局,将数据成员用一个指针替代可以减少编译期依赖,保持 ABI 兼容
    // A.h
    //用std::unqiue_ptr代替原始指针:析构函数的定义要位于要析构的类型的定义之后
    //支持移动操作:移动操作定义必须位于要析构类型定义之后
    #include <memory>
    
    class A {
    public:
        A();
        ~A();
        A(A&&);
        A& operator=(A&&);
    private:
        struct X;
        std::unique_ptr<X> x;
    };
    
    // A.cpp
    #include "A.h"
    #include <string>
    #include <vector>
    
    struct A::X {
        int i;
        std::string s;
        std::vector<double> v;
    };
    
    A::A() : x(std::make_unique<X>()) {}
    A::A(A&&) = default;
    A& A::operator=(A&&) = default;
    A::~A() = default;
    
    // A.h
    //使用std::shared_ptr 
    #include <memory>
    
    class A {
    public:
        A();
    private:
        struct X;
        std::shared_ptr<X> x;
    };
    
    // A.cpp
    #include "A.h"
    #include <string>
    #include <vector>
    
    struct A::X {
        int i;
        std::string s;
        std::vector<double> v;
    };
    
    A::A() : x(std::make_shared<X>()) {}
    
  • 相关阅读:
    【探路者】团队Alpha周贡献分数分配结果
    2017秋-软件工程第七次作业-第八周例行总结
    2017秋-软件工程第七次作业(2)-【探路者】Alpha周(2017年10月19)贡献分配规则和分配结果
    2017秋-软件工程第七次作业(1)-【探路者】贪吃蛇阿尔法发布展示(视频展示)
    2017秋-软件工程第七次作业(1)-【探路者】贪吃蛇阿尔法发布
    名词1
    Jsp1
    代码,python1
    关于键盘
    代码,java_web
  • 原文地址:https://www.cnblogs.com/Asumi/p/12452940.html
Copyright © 2020-2023  润新知