【读书笔记】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的类型推断是与expr和ParamType有关
- 以下情况不适用
- 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>()) {}