Effective c++

模板推导

对于传数组,由于c中数组和指针是等价的,因此,数组通过值传递的时候被视为一个指针,模板推导出来也是一个指针。然而,当作为引用传递的时候,其就不和指针等价了,可以推导出其类型和大小。

但实际上,这两个传进去的东西用起来是一样的。都是函数()。

auto

1
2
3
4
template<typename T>
void f(ParamType param);

ParamTypeOfAuto param;
JSX

这两个实际上是等价的,除了一种情况。

当使用{}初始化的时候,auto会默认为std::initialize_list,而模板推导则不会默认,模板推导接受{}是必烂的。而如果是构造,如果这个{}能够符合两个类型的构造,那么就是歧义的。

返回类型的auto和lambda的形参auto虽然看着是auto,但实际上使用的规则是模板推导的规则而非auto的规则。

另外,lambda的形参使用auto相当于就是模板函数。

使用auto能够让类型上的依赖自动推导,不用牵一发而动全身。

decltype

decltype不是类型推导,decltype会忠诚的返回变量本身的类型。

然而,对于一个非引用值来说,如果他是复杂的表达式,即便是(param),param是T类型,也会让整个类型的推导变为T&。

由于auto是类型推导,为了让其使用decltype的规则,就应该使用decltype(auto),这个时候会把推导方法使用decltype的规则。

推导

编译器或者typeid产生的推导可能是不正确的,还是靠编译器编译的时候的解释比较靠谱。一种比较简单的方式是

使用auto的好处

  1. 避免没有初始化
  2. 省略冗长类型名
  3. 避免不必要的类型转换(如在不同平台typedef实际不同)及其产生的相关问题

不使用auto的场合

由于c++不允许对bit的引用,但是vector是按bit存放的,因此,返回的并不是bool&,而是一个代理类,有着类似的功能。

然而,不显式的代理类一般是不被平常使用的,他是隐藏于使用但我们并不知道他被使用了,我们也不会主动去使用他,因为这可能会导致一些错误,就像工具人一样。

而使用auto会让这些代理类被显式使用,会导致错误。

为了贯彻auto的理念,使用 auto a=static_cast(b)来显式类型初始化惯用法。

而这也表明了,b可能不是这个类型,而我希望让他转换成type这个类型的意思。

()和{}

  • {}的好处

    在任何情况下都可以用

    避免了类型变窄的隐式转换

    避免了默认初始化可能会被误判成函数声明的问题

  • {}的坏处

    如果定义了以initialized_list为参数的构造函数(要避免),那么所有能够匹配到initialized_list的都会使用这个构造函数

    当通过模板完美转发的时候,不知道使用者到底是想要使用{}还是()

使用nullptr

使用别名声明

  1. using看起来比typedef更易懂
  2. using支持模板,而typedef应该是一个宏,所以不支持

为什么要加typename的原因是::所得的成员不一定是一个类型,可能是一个实际成员。

使用限定域enum

避免了作用域的问题。

避免了隐式转化。

限定域enum默认最为int大小,而非限定没有默认,因此不能声明和定义分开。

而对于用来表示对应数字的问题,如对于tuple的get来说,是比较麻烦的,但为了贯彻限定域enum,可以

使用deleted

deleted能够达到与声明为private一样的效果。

  1. 能够在编译的时候就检测出来,而非在链接的时候。
  2. 针对任何函数
  3. 能够用来对于函数的参数进行过滤,虽然现在大概应该要用concept更好把

使用override

基类和派生类函数名必须完全一样(除非是析构函数),因此构造函数不能继承。

使用const_iterator

使用noexcept

移动,swap和析构都需要是noexcept的。

一个函数时候应该是noexcept应该要衡量noexcept带来的性能上的优势与实现noexcept带来的性能上的劣势。

另外,noexcept函数并不要求其调用的函数是noexcept(这是为了兼容老代码)。

使用constexpr

constexpr保证了变量是编译器可知的,因此编译器就会在编译期把他求出来,一个constexpr变量一定是const的。

constexpr函数则表示,这个函数的返回值可能是编译期可知的,也就是,可以用非constexpr参数去调用这个函数,返回的也不是constexpr的,但是,如果用constexpr的形参去调用,则返回的结果也是constexpr的,但不一定是const的。

所以如果一个函数的调用的形参是constexpr的,那么这个函数就应该被声明为constexpr的。

保证const函数线程安全

大概是为了保证其他函数的const性,所以某些函数也要被生成为const,至少表面看上去是const,而为了在里面修改内容,就需要用mutable

由于const函数是只读的,因此对于多线程是可以并行的。所以如果有mutable,那么就要进行保护,如使用atomic或者mutex。

一般来说atomic比mutex快,但是,对于多个mutable同时操作,atomic就并不能保证安全了,只能用mutex。

默认函数的生成

对于默认生成的移动函数来说,对于所有成员都会进行移动请求,可以移动的就会移动,但如果无法移动就会拷贝

拷贝的两个函数的生成是互不影响的。

移动当且仅当5大函数都没有声明的时候才会生成。

这些的逻辑是,如果声明了,那么就说明他和默认的不一样,那么连带着其他的也可能不一样

unique_ptr

比如说pimpl或者工厂都会使用unique_ptr。

shared_ptr

如果通过一个shared_ptr的this来构造shared_ptr,那么会烂掉。

这种情况要让这个被shared_ptr管理的类继承于enable_shared_from_this。

shared_ptr和unique_ptr都是原子的。

用make

使用make保证了当分配内存的时候他就必然已经被管理了。而new返回的指针来构造则并不能保证这一点。这样才是异常安全的。

但make不能自定义deleter。

make中使用的完美转发也会导致()和{}的内部使用问题。

对于重载了new和delete的类不能使用make。

Pimple和ptr

如果一个类中的成员的类型依赖于其他文件,这样就会导致依赖库的变化会导致整个的变化。

因此一般声明一个struct,并让类拥有一个指向这个struct的指针。

而这个struct的定义在cpp中实现,这样依赖就不在.h中了,大大减少了编译时间。

一般来说,这个指针用unique_ptr。

但是,不能使用默认生成的移动函数和析构函数。因为,这二者都会调用unique_ptr的delete,但是他的delete由于是一个类型参数,因此会去判断删除的类型是不是一个不完整类型,很显然,是的。

因此,二者的定义就必须延迟到struct定义完后才行,因此要在.h中声明,在.cpp中定义。

而shared_ptr就没有这个问题,因为他的删除器并不是一个模板参数。

std::move和std::forward

实际上只是进行了类型转换。std::move是不管左值还是右值都转换为右值。

而std::forward是只有当传入为右值时才将这个左值的右值转换为右值。

然而,这种转换并没有对cv做任何操作。因此,如果原本是一个const对象,并调用了std::move并去初始化对象,调用的会是拷贝构造函数,因为const还在。

通用引用

当带有类型推导的时候的&&,如auto&&和T&&都是通用引用,代表既有可能是左值也有可能是右值。

cv的出现也会让通用引用失效,如const auto&&就是烂的。

对通用引用用forward,对右值引用用move

如果在一个函数中,按值返回一个右值或者通用引用的形参,那么应该对这个返回值进行forward或者move,为了使他进行的是移动而非是拷贝。因为他是一个左值引用或者对右值的左值引用。

而如果返回的是非引用变量,就不应该这么做,因为有复制消除。

不要对通用引用进行重载

有的时候,为了转发一个参数,我们使用了通用引用。然而我们对这个函数进行重载了一个版本,仅仅将这个通用引用换成了一个普通的类型,然而,调用这个函数的时候,仍然会匹配到通用引用的版本。并且,如果一个构造函数只用了通用引用的单参数函数,那么这甚至可能会抢夺拷贝构造函数的饭碗。

解决方案

  1. 使用const T&
  2. 值传递,见倒数第二个点
  3. concept限制类型来进行重载

完美转发可以转发任意类型,因此,如果某种类型是非期待的,可能会由于完美转发导致到达很深的地方才产生错误,导致麻烦,因此可以在完美转发之前使用static_assert对这些情况进行更有效的错误汇报。

引用折叠

只有&& &&是&&,其他都是&。

移动操作的缺点

有的时候移动操作不一定会比普通的拷贝快多少,比如对一个数组的拷贝。

完美转发不能起效的情况

  1. 一个Args&&…的模板,传入一个{…},会被认为无法推导。

  2. 在转发的时候想转给一个指针,但是传入了0或者NULL,此时被推导为int。

  3. 对于一个static const的类成员,它是没有地址的,如果对它转发,也会烂。

  4. 转发函数的时候如果是直接通过函数名来指定函数,那么,如果这个函数名能代表多个函数,就会推导失败,无法判断是哪一个函数。普通的通过函数名来判断是因为有形参类型作为依据。

    因此,要显式的指明这个函数名代表的函数对应的函数类型。

  5. 位域也不行。

不要使用默认捕获

捕获的变量可能为悬空。

捕获只会对自动存储器变量进行捕获。

  1. 如果是引用捕获,那么,如果离开了这个作用域而使用这个lambda,那么就会悬空。
  2. 如果是值捕获,确实没有1中的问题,但如果捕获的是指针,那么也会产生悬空。

如果在成员函数中使用lambda进行捕获,除了局部变量以外,并不会捕捉到成员,即便它是可见的。使用这些成员的方式是,捕获this指针。[this]代表按引用捕获,[*this]代表按值捕获。当使用&默认捕获时,会捕获。但用=时则不会捕获。

解决方法是,初始化捕获。[变量名=初始化值或&变量名=初始化变量],每个之间用,隔开,依然是引用或者值传递,但这样,就可以不限于只捕获局部变量了。

对于静态变量,是不被捕获的,如果在lambda所在作用域可见,那么在lambda中也可见,就像全局变量一样,外部的变化也会影响lambda。

这些依赖如果使用默认捕获不够明确,因此应该显式的捕获。

使用初始化捕获

如果是不支持初始化捕获的,那么可以使用类似bind的方法来实现移动。

对auto&&使用decltype和forward

一个泛型lambda可以放进任何其实例化类型的function中,只不过放入后就类型确定了。

如果要对auto&&捕获所得变量进行转发,由于不存在T类型,但是forward必须要指定对应类型,因此此时可以使用decltype(变量),如果是auto&&… param,那么可以std::forward<decltype(param)>(param)…

优先用lambda而非std::bind

使用基于任务的而非线程的并行

线程就是std::thread

而任务是std::async

std::thread不会去要求其返回值,然而std::async可以通过std::future去获取他的返回值。这样也能够捕获任务的异常,而std::thread就不能。

std::thread是手动开一个线程进行跑,线程的分配是我们自己进行掌管的,然而,当负载很大的时候,我们实际上并不能非常好的进行分配,从而产生性能上的问题,不如将这个任务交给标准库,让他进行管理,通过std::async就能实现,他不一定会开一个新的线程,而是根据情况来决定,而通过策略的改变也能进行调整。

除非需要访问非常基础的线程API或者需要且能够优化应用的线程使用

如果有异步的必要则要用std::thread::async

  • std::async的默认启动策略是异步和同步执行兼有的。
  • 这个灵活性导致访问thread_locals的不确定性,隐含了任务可能不会被执行的意思,会影响调用基于超时的wait的程序逻辑。
  • 如果异步执行任务非常关键,则指定std::launch::async

保证所有线程在结束的时候是不可结合的

不可结合的包括,thread中没有包含内容,或者已经join或detach的。

如果对于一个可结合的线程隐式join或者detach都是非常恐怖的,因此,标准中如果对可结合的线程析构将会直接抛出异常。

可以使用RAII。

而在一个类中存储一个thread的时候,将thread放在最后,保证析构的异常安全。

条款38,待补充

条款39,待补充

volatile和atomic

atomic能保证操作的原子性和对代码重排顺序的限制(一种弱序关系),而volatile不行。

volatile则用来保证对这一块内存的操作不被优化。一般来说,如果两句语句之间没什么关系,那么编译器就会进行重排优化,重排完后,可能出现很多连续对同一个变量进行冗余访问和存储,因此就会被优化。

但是对一些特殊内存,如IO接口,实际上内容一直是在变化的,那么此时这些“冗余”访问和存储就不一定是冗余的了,不能被优化。

对移动成本低的且总是拷贝的形参使用值传递

一般为了处理左值和右值且高效,要重载两个函数或者使用通用引用。

然而有的时候使用值传递可能更便捷且增加不了多少开销。

值传递进来,并移动进去。

  1. 不可拷贝的值传递相当于进行了构造,不可接受
  2. 移动成本低,因为会增加一次移动成本
  3. 总是拷贝,因为如果不总是拷贝,那么按理来说就不会构造一个新的变量,引用确实没有构造,然而,值传递构造了。

有的时候,赋值操作会重用内存空间,但移动会增加内存申请和销毁的费用,这时候也不应该值传递。

在需要高性能的情况下,也不应该使用,因为移动的成本是不好估计的。

使用emplace

更快,但是要保证传入的实参是正确的


Effective c++
https://lhish.github.io/project/Effective c++/
作者
lhy
发布于
2024年6月30日
许可协议