C++ Concurrency in Actions -注解1 -附录A

今年春节放假的整整二月份, 正好在家哪儿都去不了。我逼自己用流水帐的方式读完了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的一部分;

什么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的原因:
        1. 改变functoin的accesiblity: compiler默认生成的是public的, 只有由用户自己声明才可能是private, protected的;
        1. default constructor的需求; 即使是定义了其他的constructor, copy constructor时, 当需要default constructor的行为时, 需自己显式指定;
        1. 当需要指定destructor virtual时, 同时让compiler生成默认行为;
        1. 当需要指定特定形式的函数声明, 且需要默认的行为, e.g. 声明一个参数非const的copy constructor;
        1. 对于程序需要用到只有当compiler-generated函数才特有的特性,而自己实现的函数没有那些特性的时候; e.g. trivial type的内存layout;
    • 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; 

}

// 未完待续

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值