读《effective modern c++》笔记总结

本文详细介绍了C++中的类型推导,包括模板类型推导、auto的使用以及通用引用。讨论了不同情况下的参数类型推导,如指针、引用和非引用类型。此外,还涉及了智能指针std::unique_ptr和std::shared_ptr的使用,以及std::move和std::forward在右值引用中的作用。文章强调了正确理解和使用这些概念对于编写高效C++代码的重要性。
摘要由CSDN通过智能技术生成


一、类型推导与auto

模板类型推导

针对 模板函数:

template<typename T>
void f(ParamType param)

调用:

f(expr);

接下来针对ParamType类型的三种情况:

  • ParamType是一个指针或引用,但不是通用引用
  • ParamType是一个通用引用
  • ParamType即不是指针也不是引用

这 三种情况,来理解看模板类型T的推导

ParamType是一个指针或引用,但不是通用引用

直接看例子:
模板声明:

template<typename T>
void f(T& param) //param是一个引用

变量声明:

int x=27;
const int cx=x;
const int&  rx=cx;

调用模板函数:

f(x); //T是int,param类型是int&
f(cx);//T是const int,param类型const int&
f(rx);//T是const int,param的类型是const int &

规律:
(1)当传递一个const对象给一个引用类型的参数时,传递的对象保留了常量性(向T&类型的参数传递const对象是安全的)
(2)如果expr类型是一个引用,将忽略引用部分

ParamType是一个通用引用

直接看例子:
模板声明

template<typename T>
void f(T&& param); //param现在是一个通用引用类型

变量声明:

int x=27;
const int cx=x;
const int& rx=cx;

调用模板函数:

f(x);//x是左值,所以T是int&,param是int&
f(cx);//cx是左值,所以T是const int&,param类型是const int&
f(rx);//rx是左值,所以T是const int&,param类型是const int&
f(27);//27是右值,所以T是int,param类型是int&&

规律:
(1)如果expr是左值,T和ParamType都会被推导为左值引用(唯一一种T和ParamType都被推导为引用的情况)
(2)如果expr是右值,推导规则为上面那个

ParamType即不是指针也不是引用

直接看例子:
模板声明:

template<typename T>
void f(T param); //以传值的方式处理param

变量声明:

int x=27;
const int cx=x;
const int& rx=cx;

调用模板函数 :

f(x);//T和param都是int
f(cx);//T和param都是int
f(rx);//T和param都是int

规律:
(1)只有在传值给形参时才会忽略常量性和易变性

数组实参

在普通写程序中,数组经常会退化为指向它的第一个元素的指针 ,比如:

const char name[]="J.P.Briggs"; //name的类型是const char[13]
const char* ptrToName=name;//数组退化为指针

直接看这组讲解的例子:
模板声明:

template<typename T>
void f(T param);

调用模板函数:

f(name);//name是一个数组,但是T被推导为const char*

将模板声明改成如下:

template<typename T>
void f(T& param);

调用模板函数:

f(name);//传数组,T被推导为const char[13],param被推导为const char(&)[13]

规律:
(1)对模板声明为一个指向数组的引用可以在模板 函数中推导出数组的大小

函数实参

直接看例子:
模板声明:

template<typename T>
void f1(T param); //传值

template<typename T>
void f2(T& param); //传引用

函数声明:

void someFunc(int,double); //someFunc是一个函数,类型是void(int,double)

使用模板函数:

f1(someFunc);//param被推导为指向函数的指针 ,类型是void(*)(int,double)
f2(someFunc);//param被推导为指向函数的引用,类型为void(&)(int,double)

auto类型推导

auto的类型推导除了一个例外,其他情况都和模板类型推导一样。接下来主要说这个特例:
变量声明:

auto x1=27;
auto x2(27);//类型int,值是27,同上
auto x3={27};//类型是std::initializer_list<int>,值是{27}
auto x4{27};//同上

当用auto声明的变量使用花括号进行初始
化,auto类型推导会推导出auto的类型为 std::initializer_list。
但大括号里面的变量类型不能不一样,如:

auto x5={1,2,3.0}; //错误!auto类型推导不能⼯作

对于花括号的处理是auto类型推导和模板类型推导唯一不同的地方。当使用auto的变量使用花括号的语法进行初始化的时候,会推导出std::initializer_list的实例化,但是对于模板类型推导这样就行不通:

auto x={11,23,9}; //x的类型是std::initializer_list<int>
template<typename T>
void f(T param);
f({11,23,9}); //错误!不能推导出T

二、decltype的理解

decltype可以通过一个 名字或者 表达式推断出类型。
举例:

const int i=0; //decltype(i)是const int
bool f(const Widget& w); //decltype(w)是const Widget&

三、优先考虑auto而非显示类型声明

auto变量从初始化表达式中推导出类型,所以我们必须初始化
std::function 是⼀个C++11标准模板库中的⼀个模板,它泛化了函数指针的概念。
实例化 std::function 并声明⼀个对象这个对象将会有固定的⼤小。当使用这个对象保存⼀个闭包时它可能大小不足不能存储,这个时候 std::function 的构造函数将会在堆上⾯分配内存来存储,这就造成了使用 std::function 比auto会消耗更多的内存

四、区别使用()和{}创建对象

c++11使用统一初始化来整合这些混乱且繁多的初始化语法(括号初始化叫统一初始化
使用花括号,指定一个容器元素很容易:

std::vector<int> v{1,3,5}; //v包括1,3,5

括号初始化也能被用于为非静态数据成员指定默认初始值

class Widget{
...
private:
  int x{0}; //没问题,x初始值为0
  int y=0; //同上
  int z(0);//错误
}

不可拷贝的对象可以使用花括号初始化或者小括号初始化,但是不能使用"="初始化:

std::vector<int> ai1{0}; //没问题,x初始值为0
std::atomic<int> ai2(0); //没问题
std::atomic<int> ai3 = 0; //错误!

由上看出,几种初始化方式只有括号任何地方都能被使用。
括号表达式有一个异常的特性,它不允许内置类型隐式的变窄转换(narrowing conversion)。如果一个使用了括号初始化的表达式的值无法用于初始化某个类型的对象,代码就不会通过编译:

double  x,y,z;
int sum1{x+y+z};//错误,三个double的和不能用来初始化int类型的变量
int sum2(x+y+z);
int sum3=x+y+z;//同上

C++规定任何能被决议为⼀个声明的东西必须被决议为声明。这个规则的副作用是让很多程序员备受折磨:当他们想创建⼀个使⽤默认构造函数构造的对象,却不小心变成了函数声明
如:
想使用一个实参调用一个构造函数,可以如下:

Widget w1(10); //使⽤实参10调⽤Widget的⼀个构造函数

但如果你尝试使⽤⼀个没有参数的构造函数构造对象,它就会变成函数声明:

Widget w2(); //最令⼈头疼的解析!声明⼀个函数w2,返回Widget

由于函数声明中形参列表不能使用花括号,所以使用花括号初始化表明你想调用默认构造函数构造对象就没有问题:

Widget w3{}; //调⽤没有参数的构造函数构造对象

还有两点注意的就是:
(1)auto对于花括号的推导,看上面
(2)vector对于()和{}区别

五、优先考虑nullptr而非0和NULL

六、优先考虑别名声明而非typedefs

c++11提供了一个别名声明:

using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;

当声明⼀个函数指针时别名声明更容易理解:

// FP是⼀个指向函数的指针的同义词,它指向的函数带有int和const std::string&形参,不返回任何东
西
typedef void (*FP)(int, const std::string&); // typedef
//同上
using FP = void (*)(int, const std::string&); // 别名声明

需要特别注意的一点:别名声明可以被模板化,但是typedef不能

template<typename T>
using MyAllocList = std::list<T,MyAlloc<T>>;
MyAllocList<Widget> lw;

七、优先考虑限域枚举而非未限域枚举

八、优先考虑使用deleted函数而非使用未定义的私有声明

deleted函数不能以任何方式被调用,即使你在成员函数或者友元函数里面调用deleted 函数也不能通过编译
注意,deleted 函数被声明为public而不是private;因为,C++会在检查 deleted 状态前检查它的访问性。当客户端代码调用一个私有的 deleted 函数,⼀些编译器只会给出该函数是private的错误,而没有诸如该函数被 deleted 修饰的错误。因此,如果要将老代码的"私有且未定义"函数替换为 deleted 函数时请⼀并修改它的访问性为public,这样可以让编译器产生更好的错误信息。

deleted 函数还有⼀个重要的优势是任何函数都可以标记为 deleted(包括非成员函数)
deleted 函数还可以(private成员函数做不到的地方)禁止一些模板的实例化。

九、使用override声明重载函数

重写与重载区分;
首先针对,重写,需要满足的要求:

  • 基类函数必须是virtual
  • 基类和派生类函数名必须完全一样(除非是析构函数)
  • 基类和 派生类函数参数必须完全一样
  • 基类和派生类函数常量性(constness)必须完全一样
  • 基类和派生类函数的返回值和异常说明(exception specifications)必须兼容
  • 函数的引用限定符必须完全一样;成员函数的引用限定符是C++11很少抛头露脸的特性,所以如果你从没听过它无需惊讶。它可以限定成员函数只能用于左值或者右值

如下,对于限定符 :

class Widget {
public:void doWork() &; //只有*this为左值的时候才能被调⽤
void doWork() &&; //只有*this为右值的时候才能被调⽤
};
…
Widget makeWidget(); // ⼯⼚函数(返回右值)
Widget w; // 普通对象(左值)
…
w.doWork(); // 调⽤被左值引⽤限定修饰的Widget::doWork版本
// (即Widget::doWork &)
makeWidget().doWork(); // 调⽤被右值引⽤限定修饰的Widget::doWork版本
// (即Widget::doWork &&)

需要注意的是,如果基类的虚函数有引用限定符,派生类的重写就必须具有相同的引用限定符。如果没有,那么新声明的函数还是属于派生类,但是不会重写父类的任何函数。

override会显式的将派生类函数指定为应该是基类重写版本,保证派生类虚函数结果是我们想要的,如果不是会报错(程序的健壮性)

final

十、特殊成员函数的生成

特殊成员函数是c++自己生成的,c++98有四个:默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符函数;并且这些函数仅在需要的时候才生成,且是隐式public且inline。
c++11又有了两个:移动构造函数和移动赋值运算符
如下:

class Widget {
public:
...
Widget(Widget&& rhs);
Widget& operator=(Widget&& rhs);
...
};

对于移动构造而言,一旦生成就会对非static数据执行逐成员的移动,核心是对对象使用 std::move,然后函数决议时会选择执行移动还是拷贝操作,如果支持移动就会逐成员移动类成员和基类成员,如果不支持移动就执行拷贝操作就好了。
如果你声明了某个移动函数,编译器就不再生成另⼀个移动函数。
如果你声明了某个移动函数,就表明这个类型的移动操作不再是“逐⼀移动成员变量”的语义,即你不需要编译器默认⽣成的移动函数的语义,因此编译器也不会为你生成另⼀个移动函数。
再进⼀步,如果⼀个类显式声明了拷贝操作,编译器就不会生成移动操作。这种限制的解释是如果声明拷⻉操作就暗⽰着默认逐成员拷⻉操作不适⽤于该类,编译器会明⽩如果默认拷⻉不适⽤于该类,移动操作也可能是不适⽤的。这是另⼀个⽅向。声明移动操作使得编译器不会⽣成拷⻉操作。编译器通过给这些函数加上delete来保证

十一、 智能指针

独占资源型指针std::unique_ptr

默认情况下,std::unique_ptr 等同于原始指针
因此, std::unique_ptr 只支持移动操作,且移动操作将所有权从源指针转移到目的指针。
std::unique_ptr 有两种形式,一种用于单个对象( std::unique_ptr ),一种用于数组( std::unique_ptr<T[]> )

共享资源型指针std::shared_ptr

std::shared_ptr 通过引用计数来确保它是否是最后⼀个指向某种资源的指针,引用计数关联资源并跟踪有多少 std::shared_ptr 指向该资源。 std::shared_ptr 构造函数递增引用计数值,析构函数递减值,拷贝赋值运算符可能递增也可能递减值。
引用计数影响的性能问题如下:

  • std::shared_ptr大小是原始指针的两倍,内部包含一个指向资源的原始指针,还包含一个资源的引用计数值;
  • 引用计数必须动态分配
  • 递增递减引用计数必须是原子性

移动 std::shared_ptr 会比拷贝它要快:拷贝要求递增引用计数值,移动不需要
指定自定义销毁器不会改变std::shared_ptr对象的大小。不管销毁器是什么,一个std::shared_ptr对象都是两个指针大小
std::shared_ptr对象的内存是这样的:
在这里插入图片描述
控制块创建遵循的规则:

  • std::make_shared总是创建一个控制块。它创建⼀个指向新对象的指针,所以可以肯定 std::make_shared 调⽤时对象不存在其他控制块。
  • 当从独占指针上构造出std::shared_ptr时会创建控制块(即std::unique_ptr或者std::auto_ptr)std::shared_ptr 侵占独占指针所指向的对象的独占权,所以std::unique_ptr 被设置为null
  • 当从原始指针上构造出std::shared_ptr时会创建控制块。但是用std::shared_ptr 或者std::weak_ptr 作为构造函数实参创建 std::shared_ptr 不会创建新控制块,因为它可以依赖传递来的智能指针指向控制块。

但是需要注意的是,从原始指针上构造超过⼀个 std::shared_ptr,容易造成同一个原始指针有多个引用计数值,每一个最后都会变成,然后最终导致原始指针销毁多次,第二次销毁开始产生未定义行为
如下:

auto pw = new Widget; // pw是原始指针
…
std::shared_ptr<Widget> spw1(pw); // 为*pw创建控制块
…
std::shared_ptr<Widget> spw2(pw); // 为*pw创建第⼆个控制块

对于这种情况,两点建议:

  • 避免传给 std::shared_ptr 构造函数原始指针。通常替代方案是使用std::make_shared
  • 如果你必须传给 std::shared_ptr 构造函数原始指针,直接传new出来的结果,不要传指针变量。
std::shared_ptr<Widget> spw1(new Widget); // 直接使⽤new的结果

控制块通常只占个word大小,自定义销毁器和分配器可能会让它变大一点。通常控制块的实现比你想的更复杂⼀些。它使用继承,甚至里面还有⼀个虚函数(用来确保指向的对象被正确销毁)。这意味着使⽤ std::shared_ptr 还会招致控制块使用虚函数带来的成本。

std::shared_ptr 不能处理的另一个东西是数组。

std::weak_ptr解决std::shared_ptr悬空问题

std::weak_ptr是一个类似std::shared_ptr但不影响对象引用计数的指针

std::weak_ptr 的潜在使用场景包括:caching、observer lists、打破 std::shared_ptr 指向循
环。

优先考虑使用std::make_unique和std::make_shared而非new

std::make_unique 和 std::make_shared 有三个make functions中的两个:接收抽象参数,完美转发到构造函数去动态分配⼀个对象,然后返回这个指向这个对象的指针。第三个make function 是std::allocate_shared. 它和 std::make_shared ⼀样,除了第⼀个参数是用来动态分配内存的对
象。

十二、std::move和std::forward

对于移动语义和完美转发:

  • 移动语义用移动操作来代替昂贵的复制操作。正如复制构造函数和复制赋值操作符给了你赋值对象的权利⼀样,移动构造函数和移动赋值操作符也给了控制移动语义的权利。移动语义也允许创建只可移动(move-only)的类型,例如 std::unique_ptr , std::future 和std::thread 。
  • 完美转发使接收任意数量参数的函数模板成为可能,它可以将参数转发到其他的函数,使目标函数接收到的参数与被传递给转发函数的参数保持⼀致。

记住,参数(parameter)永远是左值(lValue)

std::move 和 std::forward 仅仅是执行转换(cast)的函数(事实上是函数模板)。 std::move 无条件
的将它的参数转换为右值,而 std::forward 只在特定情况满足时下进行转换。
std::move 除了转换它的参数到右值以外什么也不做,避免了一次复制操作的代价
注意,移动构造函数只接受⼀个指向非常量(non-const) 的右值引用

  • 不要在移动对象的时候,声明他们为常量。对常量对象的移动请求会悄无声息的被转化为复制操作。
  • std::move 不仅不移动任何东西,而且它也不保证它执行转换的对象可以被移动。关于 std::move ,你能确保的唯一 一件事就是将它应用到一个对象上,你能够得到一个右值。

std::forward 是一个有条件的转换:它只把由右值初始化的参数,转换为右值。
std::move 的使用代表着无条件向右值的转换,而使用std::forward 只对绑定了右值的引用进行到右值转换。这是两种完全不同的动作。前者是典型地为了移动操作,而后者只是传递(亦作转发)⼀个对象到另外⼀个函数,保留它原有的左值属性或右值属性。

区分通用引用与右值引用

T&& 有两种不同的意思:

  • 右值引用,只绑定到右值上,并且它们主要的存在原因就是为了声明某个对象可以被移动
  • 既可以是一个右值引用,也可以是一个左值引用。这种引用在源码里看起来像右值引用(也即 T&& ),但是它们可以表现得它们像是左值引用(也即 T& )。(二重性)

出现通用引用的情况:(存在类型推导)

  • 函数模板参数
template <typename T>
void f(T&& param); //param是⼀个通⽤引⽤
  • auto声明符
auto&& val2 = var1; //var2是⼀个通⽤引⽤

如果T&& 不带有类型推导,那么它就是⼀个右值引用。

void f(Widget&& param); //没有类型推导
//param是⼀个右值引⽤
Widget&& var1 = Widget(); //没有类型推导
//var1是⼀个右值引⽤

通用引用是引用,所以它们必须被初始化。一个通用引用的初始值决定了它是代表了右值引用还是左值引用。

template <typename T>
void f(T&& param); //param是⼀个通⽤引⽤
Widget w;
f(w); //传递给函数f⼀个左值;参数param的类型
//将会是Widget&,也即左值引⽤
f(std::move(w)); //传递给f⼀个右值;参数param的类型会是
//Widget&&,即右值引⽤

对T&&排除是通用引用的情况:

  • 必须保证是T&&,否则就不是通用引用。如下:
template <typename T>
void f(std::vector<T>&& param); //param是⼀个右值引⽤

param 的类型声明并不是 T&& ,而是一个 std::vector&& ,排除了参数 param 是一个通用引用的可能性,param 因此是⼀个右值引用。验证如下:

std::vector<int> v;
f(v); //错误!不能将左值绑定到右值引⽤
  • 出现一个简单的 const 修饰符,也会使一个引用失去成为通用引用的资格。如下:
template <typename T>
void f(const T&& param); //param是⼀个右值引⽤

T&& 是决定只绑定到右值还是可以绑定任意对象

对于,通用引用来说,主要是通过类型推导将左值和右值区分,T类型的左值被推导为&类型,T类型的右值被推导为T(非引用)

右值引用使用std::move,通用引用使用std::forward

通用引用可能绑定到有资格移动的对象上,并且通用引用使用右值初始化时,才可将其强制转换为右值。

避免在通用引用上重载

引用折叠

通用引用的模板参数的编码问题:

template<typename T>
void func(T&& param);

左值被传入时,T被推导为左值,左值被编码为左值引用;当右值被传入时,T被推导为非引用

如果一个上下文中允许引用的引用存在(比如,模板函数的实例化),引用根据规则折叠为单个引用:
如果任一引用为左值引用,则结果为左值引用。否则(即,如果引用都是右值引用),结果为右值引用

引用折叠发生的四种情况:

  • 模板实例化
  • auto变量的类型生成
  • 使用typedef和别名声明,在创建或者定义typedef过程中出现了引用的引用,则引用折叠就会起作用
typedef T&& RvalueRefToT;
  • decltype使用的情况

十三、Lambda表达式

lambda表达式就是一个表达式,

std::find_if(container.begin(), container.end(),
[](int val){ return 0 < val && val < 10; }); // 本⾏⾼亮

闭包是lambda创建的运行时对象,在上面的std::find_if 调用中,闭包是运行时传递给 std::find_if 第三个参数。
Lambda通常被用来创建闭包,该闭包仅用作函数的参数,并且通常可以拷贝,所以可能有多个闭包对应于一个lambda。如下:

{
int x; // x is local variable
...
auto c1 = [x](int y) { return x * y > 55; }; // c1 is copy of the closure
//produced by the lambda
auto c2 = c1; // c2 is copy of c1
auto c3 = c2; // c3 is copy of c2
...
}

c1, c2,c3都是lambda产生的闭包的副本。

避免使用默认捕获模式

生命周期的问题-----------》变量悬空
使用显式的局部变量和参数引用捕获方式,显式捕获能让人更容易想起“确保没有悬空变量”。
在通常情况下,按值捕获并不能完全解决悬空引用的问题。这里的问题是如果你按值捕获的是⼀个指针,你将该指针拷贝到lambda对应的闭包里,但这样并不能避免lambda外删除指针的行为,从而导致你的指针变成悬空指针。
⼀个定义在全局空间或者指定命名空间的全局变量,或者是⼀个声明为static的类内或文件内的成
员。这些对象也能在lambda里使用,但它们不能被捕获。

使用初始化捕获来移动对象到闭包中

什么是初始化捕获?
初始化捕获就是支持使用初始化器来初始化捕获的变量

auto func = [pw = std::make_unique<Widget>()] 
{ return pw->isValidated() 
&& pw->isArchived(); };

如上面的pw = std::make_unique<Widget>()

使用初始化捕获可以让你指定:

  1. 从lambda生成的闭包类中的数据成员名称;
  2. 初始化该成员的表达式;

对于std::forward的auto&&形参使用decltype

对 auto&& 参数使用decltype 来( std::forward )转发参数;

auto f = [](auto&& x)
{ return func(normalize(std::forward<???>(x))); };

把 x 完美转发给函数 normalize 。首先,x需要改成通用引用,其次,需要使用std::forward 将 x 转发到函数 normalize 。
但是在理论和实际之间存在⼀个问题:你传递给 std::forward 的参数是什么类型,就决定了上面的 ??? 该怎么修改。
⼀般来说,当你在使用完美转发时,你是在⼀个接受类型参数为 T 的模版函数⾥,所以你可以写
std::forward 。但在泛型lambda中,没有可⽤的类型参数 T 。在lambda⽣成的闭包⾥,模版化的 operator() 函数中的确有⼀个 T ,但在lambda⾥却⽆法直接使⽤它。
把 decltype(x) 传递给 std::forward 都能得到我们想要的结果

auto f =
[](auto&& param)
{return func(normalize(std::forward<decltype(pram)>(param)));
};
Coming to grips with C++11 and C++14 is more than a matter of familiarizing yourself with the features they introduce (e.g., auto type declarations, move semantics, lambda expressions, and concurrency support). The challenge is learning to use those features effectively—so that your software is correct, efficient, maintainable, and portable. That’s where this practical book comes in. It describes how to write truly great software using C++11 and C++14—i.e. using modern C++. Topics include: The pros and cons of braced initialization, noexcept specifications, perfect forwarding, and smart pointer make functions The relationships among std::move, std::forward, rvalue references, and universal references Techniques for writing clear, correct, effective lambda expressions How std::atomic differs from volatile, how each should be used, and how they relate to C++'s concurrency API How best practices in "old" C++ programming (i.e., C++98) require revision for software development in modern C++ Effective Modern C++ follows the proven guideline-based, example-driven format of Scott Meyers' earlier books, but covers entirely new material. "After I learned the C++ basics, I then learned how to use C++ in production code from Meyer's series of Effective C++ books. Effective Modern C++ is the most important how-to book for advice on key guidelines, styles, and idioms to use modern C++ effectively and well. Don't own it yet? Buy this one. Now". -- Herb Sutter, Chair of ISO C++ Standards Committee and C++ Software Architect at Microsoft
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值