【读书笔记】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\n"; 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>()) {}
posted @ 2020-03-10 01:08  甜酒果。  阅读(407)  评论(0编辑  收藏  举报