Effective Modern C++ : 3.转向现代C++

1. 条款7:在创建对象时注意区分()和{}

1️⃣眼花缭乱:(以下都没有赋值!!!!)

int a(5);
int a = 5;
int a{5};
int a = {5} //编译器会将其等同于上者

而对于自定义对象,以上更加不是学术问题,而是实际不同:

A a; //默认构造
A b = a;	//拷贝构造
b = a;	//拷贝赋值

为了解决众多初始化语法带来的困惑,C++11引入了统一初始化,基础是大括号形式:

std::vector<int> L{1, 3, 5};

2️⃣大括号可以用来为非静态成员指定默认初始化值,也可使用=,但不能使用()

class A
{
private:
	int x{0};
	int y = 0;
	int z(0); //error!!!!
};

对于不可复制的对象,则是=不行,其他可以。这些说明了为什么{}是统一初始化。

3️⃣大括号初始化,禁止进行隐式窄化型别转化:(这种转化,意味着精度丢失)

double x,y,z;
int a(x + y + z);
int b{x + y + z};//error!!!!

它还对解析语法免疫:

Widget w2(); //可能会被解析成一个函数,而不是进行默认初始化。
Widget w2{}; //无问题

4️⃣{}的问题除了auto的推导问题,还有就是其太霸道:只要有任何可能,大括号初始化物就会与带有std::initialized_list型别的形参相匹配,即使其他重载版本有着貌似更加匹配的形参表。具体讨论见书 P58。

2. 条款8:优先选用nullptr,而非0或NULL

1️⃣0会NULL都不具备指针型别(0是int,NULL一般是long)。​而nullptr可以隐式转换到所有的裸指针型别,且不具备整型型别

3. 条款9:优先选用别名声明,而非typedef

using List_int = std::vector<int>;

1️⃣优越的压倒性理由在于:别名声明可以模板化。​

template<typename T>
using AllocList = std::list<T, Mylloc<T>>;

而且这种模板化可以避免依赖型别,而带有依赖型别,则必须在前面加个typename

或者说,别名模板可以让人免写::type后缀,并且在模板内,对于内嵌typedef的引用经常要求加上typename前缀。

2️⃣<type_trait>的类型特征模板:

std::remove_const<T>::type    //由const T生成T
std::remove_reference<T>::type  //由T&,T&&生成T
std::add_lvalue_reference<T>::type  //由T生成T&

4. 条款10:优先选定限定作用域的枚举型别,而非不限定的

1️⃣第一个理由在于,不限类型的枚举(C++98风格)会泄漏到其所在作用域:

//不限定
enum Color {red, blue};
auto red = 1; //error!!!
----------------------------------------
//限定作用域
enum class Color {red, blue};
auto red = Color::red; //right!!!!

2️⃣第二个压倒性理由是:限定枚举是强型别的(也称为枚举类),不能进行隐式类型转换,而非限定的,可能在使用时,隐式转换成整数,然后进一步转换成浮点数类型。(如果实在想要进行,则进行强制类型转换)。

3️⃣都支持默认底层类型指定,限定作用域的枚举的底层类型是int,而不限定则没有默认底层类型。如果想要修改,两者都可以通过如下方式:

enum Color : std::size_t;

4️⃣关于限定枚举的一个缺点以及解决方法,还有更多详细信息,见书 P72。

5. 条款11:优先选用删除函数,而非private未定义函数

1️⃣如果定义了某个函数,却不希望用户去调用(常见的如:拷贝构造函数、拷贝赋值函数),旧有做法是,将其声明为private,然后不去定义它。

2️⃣新方法是使用删除函数,在其末尾加一个= delete。相对于private只能定义成员函数,删除函数可以应用在所有函数上。

3️⃣还有一个好处是:阻止那些不应该进行的模板具现​。举个例子:指针世界有两个个例,void*char*,因为各自的原因,和其他指针的处理方法不一样,那么就需要考虑如下模板的具现问题:

template<typename T>
void processPtr(T* ptr);
...
template<>
void processPtr(void*) = delete;
template<>
void processPtr(char*) = delete;

所以,总结一句话:请始终使用= delete

6. 条款12:为意在改写的函数添加override声明

科普向,之前还不知道啥是墨菲定律,现在知道了:只要一件事可能出错,那么它一定会出错。

引用饰词

1️⃣虚函数的改写的发生条件如下:

  • 基类的函数必须是虚函数;基类和派生类的函数名字、形参、函数常量性必须相同。

  • 函数返回值和异常规格必须兼容。

  • C++11又加了一条:函数引用饰词必须完全相同。这个又是干嘛的?他们是为了限制成员函数仅用于左值或右值:

    class A{
    public:
    	void Func()&;
    	void Func()&&;
    };
    
    ...
    
    A a;
    A.Func();		//调用Func()&,左值版本
    MakeA().Func(); //调用右值版本。
    

2️⃣正因为条件这么苛刻,所以很容易就导致没有重写,所以我们需要override来显式表明。

7. 条款13:优先选用const_iterator

具体分析见书。

8. 条款14:只要函数不会抛出异常,就为其加上noexcept声明

具体分析见书P 91。

1️⃣总结:

  • noexcept声明是函数接口的组成部分,这意味着调用方可能会对它有依赖。
  • 相对于不带noexcept的函数,带有的函数有更多机会得到优化。
  • noexcept性质对于移动操作、swap、内存释放函数和析构函数最有价值。
  • 大多数函数都是异常中立,不具备noexcept性质。

9. 条款15:只要有可能使用constexpr,就使用它

1️⃣它很令人困惑。当它应用对象时,其实就是个加强版的const,但应用于函数时,却有着相当不同的意义。表面上看,constexpr表示的是这样的值:它不仅是const,而且是编译时已知的。但应用在函数时却两者都不符合,这更是个不错的设计!

2️⃣对于对象,这是明确的:

constexpr auto a = 10;
int b[a];		//right!!!

3️⃣对于函数,我们可以总结:如果传入的参数都是constexpr(编译时已知的),那么函数的返回值也会被认为是编译时已知的(constexpr);否则和普通函数没有区别。

constexpr
int pow(int base,int exp) noexcept
{
	...
}
...
constexpr auto num = 5;
std::array<int, pow(3, num)> results; //right!

4️⃣甚至可以创建编译时已知的对象(因为构造函数可以是constexpr)。(具体见书)。规则和函数一样。​

10. 条款16:保证const成员函数的线程安全性

1️⃣对于const成员函数,理论上它不应该修改对象的数据,但是我们想想mutable,它让const对象的const成员函数可以修改标记为mutable的关键字。(当然这也是我们引入mutabel的原因)。

2️⃣在多线程条件下,会产生数据竞争的问题。一个解决方法是加入互斥量mutex

class A{

void roots() const{
 std::lock_guard<std::mutex> g(m);
 ...
}
...
mutable std::mutex m;
};

由于mutex是只移类别(move-only type),所以加入它,类A会失去复制性,但依然有移动性。

3️⃣但使用互斥量有点杀鸡焉用牛刀的感觉,一个成本更低的方法是使用std::atomic

class A{
void roots()const{
	...
	callcount++;
	...
}
...
mutable std::atomic<unsigned> callcount{0};
}

但这个对于多个线程访问量,表现很差。

4️⃣总结:使用std::atomic型别的变量会比运用互斥量提供更好的性能,但前者仅适用对单个变量或内存区域的操作。

11. 条款17:理解特种成员函数的生成机制

1️⃣新的特种成员函数:移动赋值和移动构造函数(所以不用说那些老人了吧)​。这些函数一般是publicinline、且非虚的(除了当基类的析构函数是虚函数时,派生类自动生成虚析构函数

对于那些不支持移动操作的残余,哪怕进行移动操作,实际上也是进行拷贝操作。

2️⃣两个拷贝操作是独立的,声明其中一个,不会影响编译器在我们需要时生成另外一个。但移动操作不是,例如:我们写了一个移动构造函数,那么编译器不会生成移动赋值函数。尤有进者,声明了拷贝操作,就不会生成移动操作。

我们自定义,则说明按成员复制不是我们想要的,我们可能在进行一些骚操作,而编译器咋知道,它可能会认为这个时候为我们生成移动操作,不是我们想要的结果(它可不会乘上一个矩阵,再移动或拷贝)。所以干脆不生成了。

这么一想,我们自定义移动,那么编译器也不会自动生成拷贝操作。

3️⃣大三律(具体见书)。总结来说,移动操作的生成条件如在:

  • 该类未声明任何复制操作。
  • 该类未声明任何移动操作。
  • 该类未声明任何析构函数。

未来,这样的机制也会延生伸到拷贝操作。当然,如果我们需要,可以显示声明,在末尾加上=default

A(const A&) = default;

4️⃣成员函数模板在任何情况下都不会抑制特种成员函数的生成。​

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JMXIN422

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值