crb912
3/9/2021 - 5:11 PM

每日笔记


The Rule of Zero in C++

The Rule of Zero in C++ 之前没有注意到这个细节,比如析构函数实现为空或指定为defualt, 会改变类的行为,move语义就不会生效。

这个Rule的意思是,要不就全部不实现(The Rule of Zero), 要不就实现5/6个(The Rule of Five) 。

3-11 --> 03-18

这周主要精力在都集中在C++ class, 各种构造和原理,我想我已经掌握的不错了。

C++编码建议

If a class has a pointer member, it probably needs a user-defined or deleted destructor, copy and move. 有指针成员,可能需要用户定义这些构造函数和操作。因为指针需要删除,而按成员方式的拷贝会出错。

If a class has a destructor, it probably needs user-defined or deleted copy and move;

By default, declare single-argument constructors explicit; 单个参数的默认构造函数。$5.1.2 [CG:C.46] 有时候不期望它转换,则使用这个关键字。

Provide strong resource safety; that is, never leak anything that you think of as a resource;

If a class is a resource handle, it needs a user-defined constructor, a destructor, and non-default copy operations;

Return containers by value(relying on move for efficiency). For non-value types, such as types in an inheritance hierarchy, return the object by unique_ptror shared_ptr. [CG: F.20].

swap

如果自己定义的类型copy操作的代价昂贵,swap操作内部应该使用移动操作。

用户定义的literal

constexpr complex<double> operator""i(long double arg)     // imaginary literal
{
    return {0,arg};
}

C的奇技淫巧是不是太过分了? 难怪这么多人都不愿意学C,它的小性子还怪多的。

5.4.2提到对Contianer的遍历:基于下标,基于Iterators。

而使用指针Iterators性能更优势:This iterator model (§12.3) allows for great generality and efficiency. 还有range-for的方式其实是隐式的指针迭代使用begin()和end()。很好理解generality,至于为什么efficiency呢? 因为基于index的对sequential random access containers有效,要保证 vector.size()是高效的操作,要保证实现了operator[](std::size_t)。有人提到这个性能差异聊胜于无is likely negligable or none , 因为编译器的优化。这个boy: My bad光速打脸,太搞了,不能想当然。


Do not litter! 不要乱扔垃圾,记住RAII

Also, memory is not the only resource. A resource is anything that has to be acquired and (explicitly or implicitly) released after use. Examples are memory, locks, sockets, file handles, and thread handles. Unsurprisingly, a resource that is not just memory is called a non-memory resource.

**Leaks **must be avoided in any long-running system, but **excessive resource retention **can be almost as bad as a leak. 资源的滞留和泄露一样糟糕


The compiler is obliged (by the C++ standard) to eliminate most copies associated with initialization, so move constructors are not invoked as often as you might imagine. This(copy elision) eliminates even the very minor overhead of a move. On the other hand, it is typically not possible to implicitly eliminate copy or move operations from assignments, so move assignments can be critical for performance.

在看这句话的时候没太懂,查了资料,也就懂了。主要涉及编译器copy elision的技术。Bjarne的意思,编译器消除了initialization相关的大部分copy操作,所以move的构造函数并没有按你想象的那样被总是会被调用。这种copy elision技术甚至把move很小的开销都消除了。但是这么好的技术,通常不会隐式在copy/move赋值的过程,所以程序员还要自己调用move。 就是说,copy elision优化了初始化过程,直接调用一次构造函数完成初始化,主要是对于没名字的对象;对于有名字且在free store的对象,会同样生成临时对象然后类型转换。但是编译器并没有优化赋值过程。更多关于copy elision的内容如下:

Copy elision是C++编译器优化代码的方式,wiki有不错的解释。其中,类对象的copy/move构造的实现可以被忽略,即便copy/move/destructor有副作用。

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects.

Consider:

Stock stock1 = Stock("", );

Stock stock2 = Stock("a", 12);  // constructor, 
sotck1 = Stock("a", 12);   // constructor for temp obj, and copy assignment operator

也就不难理解为什么第二句代码,比第三句有更好的性能了。因为compiler把stock2和右边的对象视为同一个,并没有发生copy assignment的操作,我们就说这个copy被消除了。What are copy elision and return value optimization? 这个回答也很优秀。


move semantic,移动并不是真正的移动,而是把对象A的资源解开,绑定到对象B上面,优化性能主要在于把指向new出来的对象的指针给改了,就像房子的过户,房子没动,但是合同的上产权人名字改变了。


03-10

笔记都在纸上,没时间再到博客上补充了,太晚了。 今天状态不错。自我提升既是学习的过程,也是忘却学习的过程。 每天都应该抽出一段散步的时间,边走走,边让自己思考和消化所学习的内容。思维就会开阔了。

今天顺手在论坛上回答了一个C++ constructor的问题, 主要是因为函数重载的ambious的编译错误。这是一个不宜察觉的错误,尤其当它发生在类的构造函数上。

C++的语言特性细节太多了,以至于许多人不愿意学这个。 不写了,洗漱,写写两个编程练习题就休息了。

03-06

Donald Knuth on work habits, problem solving, and happiness ,这篇文章记录了Knuth是如何学习的.

如何学习

  1. 先一步步探索,一点点进步,然而开始尝试迈出更大一步,看见树和森林
  2. 项目工作的方式: 先用草稿纸,计算或者画图。
  3. 使用符号和可视化工具。
  4. 拟人化,某些方面当成一个”好人“,其它方面当成”坏人“
  5. 寻找解决方案背后的哲学

阅读论文的方式

  1. 以同样的速度阅读小说和高质量的论文。
  2. 阅读期刊注重方法而不是结果,不必过于关心摘要和标题。(意味着学习别人解决问题的方式)
  3. 先搜集相关的资料和论文,在开始正式"开动"时再按一批的方式阅读。
  4. 并且集中精力在一两篇论文,构思这些概念,并预测作者在翻开下一页之前会说些什么。这样更容易理解作者为什么选择他们所选择的方法

其它

  1. 工作中某些部分没有乐趣,但是也应该忍受它。继续前进。这样就能找到有趣的那些部分了。
  2. 把不最喜欢的事情,安排在每天的最前面。这样就不会拖延。(哈哈,这么厉害的计算机之神也会拖延,我满意了)
  3. 做些志愿活动,比如去花园除除草。理由是:不能每天都一直技术工作,精疲力竭之后大脑需要休息,然而用双手从事一些简单的体力活动。志愿服务有助于我们的主要工作。

03-04

一个有趣的知识:恐龙灭亡

昨晚失眠的时候突然好奇恐龙怎么灭绝的,今天想起来了这茬,就Google了一下。大概是说在白垩纪(K)-古近纪(P)之间的时期(称为K-PG),发现这个时期的地质中的钛含量异常高,而钛在小行星中比较常见。推断有发生小行星撞地球,造成了短期的生物大灭亡,75%的物种灭绝。我想说,人作为一个特殊的动物,有现在这样的文明,简直是一个奇迹。美丽的行星地球。而我们每个个体的一生, 又多渺小而微不足道。

显式地移动和拷贝

If you want to be explicit about generating default implementations, you can:

class Y {
public:
     Y(Sometype);
     Y(const Y&) = default;   // I really do want the default copy constructor
     Y(Y&&) = default;        // and the default move constructor
     // ...
};

When a class has a pointer member, it is usually a good idea to be explicit about copy and move operations. The reason is that a pointer may point to something that the class needs to delete, in which case the default memberwise copy would be wrong. Alternatively, it might point to something that the class must not delete. In either case, a reader of the code would like to know.

简单翻译下:当类有一个指针成员,最好显示地移动或者拷贝。因为指针可能指向了类需要销毁(delete)的资源或者对象,默认的按成员拷贝的方式会出错。即便不需要delete, 阅读代码的人也该知道(就是说,也要显示的default)。很好理解,因为虽然进行了拷贝和移动,然而有的资源仍是处于共享的状态。

#### 拇指规则(rule of thumb) 又称经验规则,rule of zero。accu这篇文章写得不错: ENFORCING THE RULE OF ZERO 。It states that if a class defines a destructor it should almost always define a copy constructor and a* copy assignment operator*. 这是个好习惯,应该定义每个C++类的时候都该保持这个习惯,虽然编译器不强制我这这么做。这篇这个文章的那个Managed resources部分的List 2代码写得好秀,作者设计的这个类不允许拷贝,但允许移动。List3的代码开始没看懂,看完这篇unique_ptr 自定义deleter的写法也就懂了,主要是不熟悉这种写法。List3的deleter不是自定义的,下面还有自定义的写法:

struct Widget{  };
// ...
auto deleter = []( Widget *p ) {
    cout << "delete Widget!" << endl;
    delete p;
};
unique_ptr<Widget, decltype(deleter)> ptr{ new Widget, deleter };

rule of thumb的具体体现的意义:Polymorphic deletion / virtual functions

!!!明天再写,熬不住了,睡觉了

decltype (C++) ,获取变量/函数/成员的类型或者表达式值类别,内括号会导致表达式求值。

我有注意到一个英语的习惯表达,术语/词语的“发明”通常用“coined by sb”,不是什么invent之类的。

03-02

不容易,终于写完了这篇C++的左值和右值,几千字不到,写了好几天。写完也搞清楚了C++的很多东西,包括移动语义。明天继续读书。

刚顺手在StackOverflow上面回答了一个关于C++的问题 What expressions create xvalues? ,写得简单的英文,应该没有语言错误。


03-01

写了一点东西,解释了C++“具有身份”和“移动”的语义,分别这两篇是:

要不是英语写的不如中文准确,我都想用英文写了。还得多练习和使用英语!

02-27

今天在写这篇C++的左值和右值 ,当是笔记吧。写东西好费脑子,查资料也很费时间。拖到晚上11点了,还没写完,困了就休息。感觉还是不够专注,特别是下午容易疲惫

02-26

忙一些杂事去了,没学多少东西,笔记就不写了。

02-25

小结: 读完了 A Tour of C++的Class章节,涵盖了具体类,抽象类,虚函数这些知识。遇到问题记在纸上,阅读结束再查资料,效率高了许多。

虚函数的匹配机制

当函数调用Virtual Functions时,多个派生类(或者说子类)都实现了基类的虚函数,编译器要通过vtbl(virtual function table)纯虚函数表去选择相应的函数,才能保证不会调用出错。

虚函数调用的效率:

  1. 效率接近普通的函数调用,相差不超过25%
  2. 空间开销(space overhead) A. 每个派生类的vtbl; B.每个对象需要一个额外的指针。这么看来空间开销很小。

使用override的习惯 派生类对虚函数进行override的时候,最好显式的使用override关键字,以避免拼写错误或者让代码更明显。

抽象类的灵活性与不足:抽象类提供的函数,可以简化派生类的函数实现。抽象类函数,甚至不需要知道实参的类型,通过引用或指针就能完成相应的操作。强大的灵活性,唯一的不足就是依赖引用或者指针。

警惕抽象类的资源泄露:由于抽象类灵活的特性,在派生类的destructor函数要保证资源能被释放,这太重要了。

英语固定搭配: especially **with respect to **error handling(固定搭配with respect to=就..而言/对于)

dynamic_cast运算符,有助于判定对象是否属于某个抽象类的类型,有点像Python的is, A是B的一个实例吗?。是一个问询,可以简化代码。如果无法转换向目标类时,想要它报错,就用dynamic_cast 作用于引用类型;不希望报错就,就作用于指针类型。

unique_ptr:new出来的“裸”指针,直接从函数返回是很危险的。最好用标准库的unique_ptr,例如return unique_ptr<Shape>{new Circle{p,r}};

C++编程建议

  1. Use concrete classes to represent simple concepts;
  2. Prefer concerete classes over class hierarchies for performance-critical components; 高性能组件尽可能用具体类
  3. Make a function a member only if it needs direct access to the representation of a class;
  4. Declare a member function that does not modify the state of its object const;
  5. If a class is a container, give it an initializer-list constructor;
  6. An abstract class typically doesn’t need a constructor;
  7. A class with a virtual function should have a virtual destructor;
  8. Use dynamic_cast where class hierarchy navigation is unavoidable;类层次漫游行为不可避免时,使用它dynamic_cast
  9. Use unique_ptr or shared_ptr to avoid forgetting to delete objects created using new;