今年春节放假的整整二月份, 正好在家哪儿都去不了。我逼自己用流水帐的方式读完了C++ Concurrency in Action这本书。由于是粗读,现在需要做一些笔记用以消化这些知识。
不是翻译
网上出现过这本书的中文翻译版, 我看了一个开头, 就没看了。然后浏览了知乎上的一些评价,基本以吐槽为主。这些吐槽也符合我对这本书的直译版的印象。英文版技术书很难按章节语句本身去直译。并不是因为某些老外所说的中文准确性的问题。个人认为有2方面原因:
- 放在整本书的知识体系下, 英文同一个词能做到很好的指代性,但在直译上, 中文的词即便在一处语境上被翻译的准确,在另一处同一个词也未必有准确的指代意义.章节一多,造成读者理解困难.
- 受到直译时语法结构的限制和专业知识的限制.这两者发生在同一个的难点处,有时真的需要一个翻译高手和一个程序高手来共同搞定.
所以我觉得,还是有必要去阅读英文原版.
但这本书并不是对C++的初学者那么友好,特别是第5章之后的讲解风格.所以本着掌握知识点的目的, 我想写一份注解. 把我认为各章的重点以自我的主观理解的方式陈述出来, 也能达到理解消化知识的目的.
附录A - Brief reference for some C++11 language features
"How to use this book"部分介绍对于没有多线程编程经验的同学应该去先读附录A中的C++语法特性;我个人体验这个是必要的.C++11的库api强烈依赖于用户对rvalue, rvalue reference, move constructor, perfect forwarding, constexpr constructor, literal type, static initialization, lambda expresslion 这些概念的理解.如果这些概念没有玩熟, 读起正式章节来, 碰上某些程序细节,也许会感到是隔靴搔痒,效果就大打折扣了.
这份附录A的注解包括C++ Concurrency in Action原文以及部分我读C++ Primier 5ed的笔记以及其他扩展.
A1. Rvalue reference
- lvalue: 指在内存中有地址的标识符(identity)
- rvalue: 指直接量(literal)或临时量(tempories)(e.g. 在寄存器里的值); 对rvalue是无法取地址操作的;
- lvalue reference: 绑定到lvalue上的引用;
- rvalue reference: 绑定到rvalue上的引用;
int i = 42;
int& l = i; // 1. ok, l bind to lvalue i;
int& l1 = 42; // 2. error, l1 lvalue reference can't bind to rvalue 42;
const int& l2 = 42; // ok, rvalue can be bound to lvalue reference to const;
// 这条规则是在C++11标准之前,为了可以让临时对象对函数传参故意留的;
void print(const std::string& s);
print("hello"); // 创建临时对象string
int&& r = 42; // ok. rvalue reference can bind to rvalue 42;
- 除了在template function的参数中, lvalue, lvalue reference不能bind到rvalue reference上; e.g. 上例注释2;
A.1.1 Move semantics
- C++11引入rvalue reference的主要目的有2个: 支持move semantics, 支持perfect forwarding; 这两者在基于Thread library的编程中都被广泛使用了.
- 显式把lvalue转成rvalue的方法:
- static_cast<X&&>(x)
- std::move()
// std::move()总返回一个rvalue reference, 实际上, 它内部也是借助static_cast实现的, 另外它还借助了std::remove_reference的作用
//
// std::remove_reference的实现借助了template类的partial specialization特性, 去除引用;
// 返回绑定到object的rvalue reference函数, 所返回的是ravlue;(准确的说是叫xvalue);
template <typename T>
typename std::remove_reference<T>::type&& move(T&& __t) {
return static_cast<typename std::remove_reference<T>::type&&>(__t);
}
- 原文的例子如下:
class X {
private:
int *data;
public:
X(): data(new int[1000000]) {}
~X() {
delelte [] data;
}
// copy-constructor
X(const X& other):
data(new int[1000000]) {
std::copy(other.data, other.data + 1000000, data);
}
// move-constructor
X(X&& other):
data(other.data) {
other.data = nullptr;
}
}
///
X x1;
X x2 = std::move(x1); // move constructor
X x3 = static_cast<X&&>(x2); // move constructor
- 了解move语义在多线程库中多处使用是必要的, e.g. std::unique_ptr, std::thread, std::promise<>, std::future<>, std::package_task<>都是不可copy,仅支持move的;
- 诸如std::string, std::vector<>是即可copy,也可以move的;
- value category: C++中的表达式,包括2个属性: type和value category, type是指它们的数据类型, e.g. int, structur Foo, class Bar; value category指这个表达式的rvalue或lvalue的;
- 所以, 一个rvalue reference type的变量, 它的value category是lvalue. 这个compiler的规则导致了std::forword的存在;
int&& r = 42; // ok, r bind to rvalue 42;
int&& q = r; // error, r's value category is a lvalue, type int&& rvalue reference can't bind to lvalue;
// 在函数中的传参,也是如此, 于是导致了perfect forwarding规则的出现;
// perfect forwarding实际上在std::therad, std::bind这类函数内部是必须支持的;
A.1.2 Rvalue reference and function template
- 对template function, 当参数为T&&, 传参有2条例外规则:
- type deduction: 实参是lvalue,lvalue reference时,compiler推断T为lvalue reference.
- reference collapsing: 当由于传参间接创建了引用的引用(reference to reference), 则整体的type发生"折叠"效果:
- T& &, T& &&, T&& & => T&
- T&& && => T&&
template <typename T>
void foo(T&& t) {}
void goo(int&& t) {}
foo(42); // 实例化: foo<int>(42); T => int
foo(3.1415); // 实例化: foo<double>(3.1415); T=>double
int i = 42;
foo(i); // 实例化: foo<int&>(i); T=> int&, T&& => int& && => int&
goo(i); // compile error, 非模板函数, 传参rvalue reference can't bind to lvalue i;
// 注意:
// 声明template function参数为const T&&, 使得type-deduction规则被禁用;
- perfect forwarding的原因和需求背景:
template function的T&& rvalue reference可以保持住实参的low level constess和type; 但是, 对于实参是ravlue时, 在template function内部, 外部的实参rvalue被绑定到rvalue reference的形参变量, 该变量(e.g. 叫该变量t)t的value category性质是lvalue, 再使用t调用其他非模板函数且具备rvalue reference的参数时,导致报错;
void goo(int&& i, const int& j) {}
template <typename T>
void foo(T&& t) {
goo(t, 100); // t是lvalue;
}
int x = 200;
foo(std::move(x)); // 报错; rvalue reference can't bind to lvalue;
- 从使用向一个模板函数传入的参数,调用另一个函数,这在std::thread使用中是基本需求; 在其他C++ library的函数中, e.g. std::allocator, std::bind等也是经常使用;
void foo(int&& i, double&& d) {
std::cout << "thread start: i=" << i << ", d=" << d << std::endl;
}
int main() {
// 实参100, 3.1415被std::thread constructor内部转发
// std::thread内部的constructor实现是支持varadic template的template function实现;
//
std::thread t(foo, 100, 3.1415);
int j = 200;
double d = 1.0;
std::therad k(foo, j, d);
std::cout << "main()" << std::endl;
t.join();
k.join();
}
扩展
std::forward()的实现
- std::forward()返回的是经过reference collapsin规则处理过的结果, 可能是lvalue reference, 可能是rvalue reference,取决于reference collapsing转换规则. 由此可以保持传给外部template function的实参的value category和type的属性;
- std::foward()的调用形式是必须使用explicit template argument的形式:
std::forward<T>(t)
, 而不是像std:move(t)
这样, 所以实现函数用到了non-deduced context的概念;
template <typename T>
constexpr T&& forward(typename std::remove_reference<T>::type&& __t) noexcept {
return static_cast<T&&>(__t);
}
tempate <typename T>
constexpr T&& forward(typename std::remove_reference<T>::type& __t) noexcept {
return static_cast<T&&>(__t);
}
- non-deduced context的经典形式是template parameter出现在:: class scope符号的左边的情况. 例如:
template <typename T> struct Foo;
template <typename T> void g(typename Foo<T>::type t); // compiler并不能做任何假设Foo<T>::type与T之间有任何关联, 所以在调用g的时刻, template argument deduction不能推断出T的类型.
所以, std::forward()的使用形式需要使用explicity template argument去指定T, 由于一般是在template function内部, T是template的形参pattern的一部分;
- 有关non-deduced context, 可以参考2篇stackoverflow的帖子:
什么expression是lvalue, 什么expression是rvalue
C++ Primer 5ed上有基本陈述, 有关什么表达式是lvalue, 什么表达式是rvalue. 但那个结论是一个非常粗略的结论. 简单摘抄在这里:
lvalue:
- 返回lvalue reference的函数;
- 一些operators, e.g. assignment, subscript, deference, prefix increment/decrement operator, etc.
rvalue:
- 直接返回对象类型的函数;
- 一些表达式的结果, e.g. arithmetic, relational, bitwise, postfix increment/prefix decrement operator, etc.
抛出一个问题: 返回rvalue reference的函数的返回值是什么value category性质?
答案是xvalue, 它即属于rvalue,也属于glvalue, 它不是lvalue, 也不是prvalue. 推荐阅读VALUE CATEGORIES – [L, GL, X, R, PR]VALUES这篇文章. 该文章对这几类事物分类, 场景以及转换讲解的十分清晰.限于篇幅, 我就不列举该文章的结论或code sample了. 下面给一个我自创的例子,是无法C++ Primer 5ed的粗略结论判断的:
#include <iostream>
using bar = int(int);
typedef int FC(int);
int hoo(int i) {
return i;
}
// when the returned value is rvalue reference to function, it is lvalue;
bar&& goo() {
return static_cast<bar&&>(hoo);
}
// same as goo()
FC&& koo() {
return static_cast<FC&&>(hoo);
}
struct Foo {
int data;
Foo(int i): data(i) {}
};
// when the returned value is rvalue reference to object, it's xvalue;
Foo&& goo2(Foo f) {
return std::move(f);
}
int main() {
// what returned by goo() is lvalue
// lvalue reference to function can bind to lvalue
bar& f = goo();
// rvalue reference to function can also bind to lvalue
bar&& g = goo();
std::cout << "goo()=" << f(10) << std::endl; // goo()=10
std::cout << "goo()=" << g(20) << std::endl; // good()=20
/
Foo ff(100), kk(20);
// Foo& hh = goo2(ff); // error, invalid initialization of non-const reference of type ‘Foo&’ from an rvalue of type ‘Foo’
// Foo&& ll = ff; // error, rvalue reference to object can't bind to lvalue;
Foo&& gg = goo2(std::move(ff));
const Foo& hh = goo2(kk);
std::cout << "gg.data=" << gg.data << std::endl; // gg.data = 20; not 100, rvalue reference 并不copy值, callstack的内容已经是第二次的变量了;
std::cout << "kk.data=" << kk.data << std::endl; // kk.data = 20;
}
- 上例有2处特殊规则:
- 返回值是rvalue reference to function的函数或表达式, 其返回值是lvalue;
- 返回值是rvalue reference to object的函数或表达式, 其返回值是xvalue;
我不再罗列更多细节了,在初步了解rvalue, lvalue之后,确实应该读一下前面推荐的博客文章,也许不必牢记,但个人体会了解是有好处的.
A2 Deleted function
-
C++11之前, 实现阻止对象copy,copy-assignment operation的方法是:
- 把copy constructor, copy assignment 放在private段;
- 并不提供copy constructor, copy assignment的实现;
-
C++11提供
=delete
声明性方法,显式的声明某个函数(不仅是constructor)是没有实现, 不可以使用的;
Move_only::Move_only(Move_only&& other):
data(std::move(other.data)) {}
Move_only& Move_only::operator=(Move_only&& rhs) {
if (this != &rhs) {
data = std::move(rhs.data); // unique_ptr only has move constructor;
}
return *this;
}
Move_only m1;
Move_only m2(m1); // error, no copy constructor
Move_only m3(std::move(m1)); // ok, move constructor
- 利用=delete声明用于控制overload:
void foo(short);
void foo(int) = delete;
foo(42); // compile error, 不可以是int
foo((short)42); // ok
A3 Defaulted functions
- defaulted function是与deleted function相反;
- deleted function是声明该函数没有实现; defaulted function则声明该函数使用compiler为之提供的默认实现;
- 当然, compiler能为之提供默认实现的函数仅有: default constructor, destructors, copy constructor, move constructor, copy-assignment operators, move-assignment operators.
-
- 需要显式自己声明defaulted的原因:
-
- 改变functoin的accesiblity: compiler默认生成的是public的, 只有由用户自己声明才可能是private, protected的;
-
- default constructor的需求; 即使是定义了其他的constructor, copy constructor时, 当需要default constructor的行为时, 需自己显式指定;
-
- 当需要指定destructor virtual时, 同时让compiler生成默认行为;
-
- 当需要指定特定形式的函数声明, 且需要默认的行为, e.g. 声明一个参数非const的copy constructor;
-
- 对于程序需要用到只有当compiler-generated函数才特有的特性,而自己实现的函数没有那些特性的时候; e.g. trivial type的内存layout;
-
- 需要显式自己声明defaulted的原因:
-
- default function声明, 联合trivial type的特性, constexpr特点, 对C++11多线程的 static intialization 的需求有重要意义; 下面这部分, 是联合讲解这一点的;
class Y
{
private:
Y() = default; // change access
public:
Y(Y&) = default; // take a non-const reference parameter copy constructor;
Y& operator=(const Y&) = default;
protected:
virtual ~Y() = default; // change access and add virtual;
}
// 未完待续