现代C++的引用操作

先要理解什么是C++的值

cppreference对Value做了解释

C++11之前,lvalue,rvalue可以简单的理解为等号两边的值类型。但C++11后,语言对用户开放了更多的辅助类型以帮助设计者开发更好用的STL库。比较典型的就是引入glvalue(general left value), xvlaue( eXpiring value, for it is temporary value at most of time, it explained in stroustroup’s article), prvalue( pure right value).

lvalue:    有identity/address (这里的identity是指可以用来和其它值用来比较的一个addressable address。不能比较的表达式意味着没有Identity), 但是这个不能被move,转移它所拥有的值或指针。通常情况下见到的变量都是lvalue。

xvalue; 既有identity,又可以被move。比如函数的返回值。

prvalue: 没有identity,但可以被move。比如某个常量值。数字1,2,常量字符串 “rock”.

StackOverFlow有一个不错的图来解释左值和右值。

Taxonomy
C++11引入的move语改变传统lvalue与rvalue概念

C++标准进一步解释了xvalue

判断一个表达式是xvalue的依据:

  • 无论是显式还是隐式的函数调用,其返回值是对象类型的右值引用(rvalue reference to object type)
  • 将右值引用强制转换(cast)为一个 对象类型。
  • 非静态非引用类型数据成员访问表达式。如 func().m; a class member access expression designating a non-static data member of non-reference type in which the object expression is an xvalue)
  • 指向成员的指针表达式,如*pbm。pb是xvalue。 (a .* pointer-to-member expression in which the first operand is an xvalue and the second operand is a pointer to data member.)

统一来讲,带名字的右值引用被当做左值,没名字的右值引用被当做xvalue;而如果引用的对象是函数的话,则不管有无名字都是左值。(In general, the effect of this rule is that named rvalue references are treated as lvalues and unnamed rvalue references to objects are treated as xvalues; rvalue references to functions are treated as lvalues whether namedornot)

And as a follow up few examples are given. Here’s an extension to them:

struct A { int m; };

A&& operator+(A, A);
A a;
A b;
a + b;   // xvalue

A&& f();
f();     // xvalue
f().m;   // xvalue

A a;
static_cast<A&&>(a);  // xvalue
std::move(a);         // xvalue

It’s a good “Move”

move本身不对其中的值进行操作,它是配合STL模板函数来完成将A容器的引用指针数据直接转交给B容器。如:

std::vector A(2,3);
std::vector B(std::move(A));

转移发生在B的右值拷贝构造函数中: std::vector(std::vector&& a){ this.data_pointer = a.data_pointer; a.reset();}

xvalue的存在就像薛定谔的猫,在某些场景下,xvalue既可以左又可以右。(这里会列出更多场景)

forward仅仅是为了在参数传递过程中,保持&&类型或&类型而引入。毕竟我们要区分到底是一个 1,还是一个int i=1。

B.S对lvaue,rvalue的解释,我认为对一般人来说不好理解。特别在解释move的解释上偏晦涩。 在编程实现上,std::move/std::forward 这对兄弟可以用来理解URef,RRef,LRef之间的关系。

// move实现 
template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }


// forward实现一个rvalue的类型传递
 template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
		    " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }

// forward实现一个lvalue的类型传递
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

类型推导

参数在推导传递过程中,允许退化(decay, reference collapse)。退化的场景有如下:

T&& -> T&
T&& & -> T&
T& && -> T&
T&& && -> T&&

因此当Foo(&)传入一个&&类型时,我们为了保持右值特性,使用forward保持。在Foo(&&)传入一个&类型时,退化为左值类型T&,也使用forward保持。

Scott Meryer 对这个问题展开了更有意思的解释

他引入一个新的概念来解释这个令人费解的特性:URef (universal reference),把URef比作随着温度而决定性别的火鸡。

  • T&& , auto&&, typedef T&&, decltype(T&&) T是一个符号,帮助类型推导。我们称它“通用引用”。 它既可以Bind一个左值(lvalue)后成为一个左值引用(LRef),也可以Bind一个右值(rvalue)成为一个右值引用(RRef)。[注意是 T]
  • 那你说, vector::push_back(T&&)是一个URef吗? 不,vector的这个函数已在编译时实例化时确定了T,如vector::push_back(Widget&&), 所以是rvalue引用。

来看几个例子:

/*auto deduction example*/
vector<int> aVector(6);    //aVectorp[5] 是一个典型的lvalue
auto&& aRef = aVector[5];  //推导为vector<int>&, URef绑定后成为LRef。
auto&  bRef = aVector[5];  //推导为vector<int>&, LRef绑定LRef。
auto&& cRef = std::move(aVector); //推导为vector<int>&&, URef绑定rvalue成为RRef

int&&  rri = 5;          // 无推导,类型为int&&
auto   autoRR = rri;     // 推导为int, 即使rri是const int&& ,去掉const和&&
auto&  autoRRR = rri;    // 推导为int&
auto&& autoRRRR = rri;   // 推导为int&
auto&& autoRRRRRR = autoRRR; //推导为int&
auto   autoX = autoRRRRRR;   //推导为int,即使autoRRRRRR是const int&&。
auto&& autoY = rri;      // 按照collapse规则,应该&&,但是这里推导为&
                         // 按照Scott的解释,rri初始化URef已经成为一个LRef
/*decltype example*/
Widget w;
decltype(w)&& r1 = std::move(w); // r1 type is Widget&&
decltype((w))&& r2 = std::move(w); // (w) is a expression,return &, r2 is Widget&

总结起来为以下:

  • type&& 并不等价于 右值引用(rvalue reference)
  • type&& 用于 语法 + 类型推导,我们称之为通用引用(URef, Universal Reference.)
  • URef最后是LRef还是RRef取决于初始化。LValue=>LRef, RValue =>RRef.
  • 并不是所有模板的T&&是URef。比如Vector::push_back的&&参数版本。
  • 重载URef通常是不对的。行为不是你所预期。
  • 类型推导和引用退化(reference collapsing)是需要URef的基本场景:模板,auto,typedef, decltype。

腾讯技术中心发表的一篇总结文章

ROS的设计

    这篇文章主要讲ROS的几个核心设计:

0,ROS的实时性设计

1, ROS如何做消息的多类型适配。(MessageFormat,MessageAdapter)

2, C++的模板和智能指针在ROS消息模块的应用。(shared_ptr)

3, 如何通过去除序列化和反序列化来增强ROS的实时性。(自定义allocator)

ROS的实时性改造

事实上,ROS1.0实时性存在很大问题。主要来源于:1,内存的频繁动态申请,包括STL。2,事件通知(condition_variable,mutex)引起的线程切换。

序列化本身只是memcpy和对应类型的赋值操作,其时间是可以有上界的。为了让用户编程方便,内部会有较多的临时堆内存,内存申请引发的page fault会是一个让车载应用不放心的地方。

另外linux本身的cfs调度是一个动态调度机制,它虽然试图给每个应用程序均分CPU时间片。但因为系统负载和应用程序之间本身的逻辑(比如通知,阻塞,抢占,同步)会导致完成一个具体的任务(比如通信任务)时间上界不可知。在不同场景下处理同一个任务的时间抖动范围也不是平稳的。

如果要确保一个Safety-Critical的应用,要从应用层面和中间件层面以及操作系统层面上都要满足时延的可预测能力。具体要解决的问题就是:内存要静态申请,阻塞任务要有超时机制,调度的时间片要完全可控。这些问题也是要求,适用到所有软件层面。

关于deterministic real-time system在车载上的应用,TTTech有篇很好的文章,详细介绍了TTTech的实现方法,值得进一步挖掘:

http://cs.uni-salzburg.at/~scraciunas/pdf/techreports/mclean_DTU19.pdf

消息的多类型适配

         ROS提供基本类型( uint8_t, uint16_t, uint32_t, double ,float)外,还提供duration,Time等自定义的定长类型。此外Header,String,Array这种变长类型的消息。

       为什么需要typename,因为在引用时,编译器需要知道class::typename 到底是嵌套类型,还是静态函数或成员变量。

       Serializer<T>::write/read接口的设计把模板的能力发挥的恰到好处,对每个基础类型的数据implement一个对应的序列化(write)/反序列化(read)操作函数。符合类型根据类型T,去找到对应的函数模板类型去Ser/deSer。

      大多数的库包括Adaptive AutoSar,DDS都是按照类似的思路完成这种操作。C++的STL编程模式在处理Serialization/DeSerialization这类功能时特别常见。

     ROS的*.msg生成的头文件中带有该类型的序列化/反序列化入口。该入口将调用serialization<T>中的函数对每个成员做序列化/反序列化。

     在回调的时候,为了适配多个参数类型,也带有Message Adapter这个类。

 

ROS中智能指针的使用

ROS中使用了weak_ptr,unique_ptr,shared_ptr, 其中shared_ptr在进程内通信的publish接口中使用了。进程内通信利用shared_ptr可以有效减少多线程数据的申请释放问题带来的bug。

邪恶的proxy

    在具有成熟规模的公司里,Proxy配置的颇为艰难。注意,我在这里说的是带用户名和密码的proxy,而不是默认proxy。 大多数应用程序的Use system proxy settings选项在这种场景下形同虚设。。

你以为你export了全局proxy变量就没事了。

结果应用A跳出来不能访问网络,因为不能使用系统proxy,你只好在应用里配置好proxy。以此类推应用B, 应用C。

你以为使用cnNTLM之类的软件在windows上把代理配置127.0.0.1:8080就没事了。。。不。。proxy不允许你这么干了。。。两边协议版本一升级可能用户自己都不知道发生了什么,也不想去折腾。。。

我的工作环境是win7+ubuntu,那apt,clion,android studio甚至firefox等等。。一一要照顾到它的proxy配置。

比如添加一个key:sudo apt-key adv –keyserver-options http-proxy=http://USER:PASSWORD@PROXY_URL:PORT/ –keyserver keyserver.ubuntu.com –recv-keys GPG_KEY

比如apt的proxy竟然要单独的创建: acquire::http::proxy “http://USER::PASSWARD#PROXY_URL:PORT”

比如android studio需要在gradle里配置proxy参数…..

不管到哪都带着一个叫做proxy的恶魔参数。。。

好歹今天你学习到一点: Sudo -E xxxx-command. 把系统环境变量带入命令行,期望大多数工具能够自己读取系统proxy吧。

只是期望。邪恶仍将无处不在。

密码保护:note-private

这是一篇受密码保护的文章,您需要提供访问密码:

远程桌面

即使是多年经验的工程师也需要尴尬的承认,配置软件环境比想象的需要多一些时间(特别是不常用的工具)

VNC

1, 使用RealVNC,TigerVNC之类的软件远程桌面。我较强的诉求是调整桌面大小。通常这类软件提供一些图像显示的depth和size设置。

RealVNC

RealVNC会推荐使用:

vncserver-virtual :1 -depth 24 -geometry 1440×900

:1 是桌面的虚拟显示器ID。登录时端口为:5900+该ID

depth: 图像显示深度

geometry: 桌面的显示大小

按照realvnc官网这篇文章介绍,你通常并不能顺利的设置桌面大小。我的环境是Ubuntu 16.04 Server(gnome,lxde,xfce)。 因为一些不知道的原因,导致这条命令配置完,使用realvnc的客户端登录上去时显示为灰色不可用的桌面。偶尔有几次却又是正常的。不得不反复去查log。

所以,退而求其次的使用

systemctl start vncserver-x11-serviced

但这种只能使用默认大小。

TigerVNC

使用tigerVNC的最新版本,根本不用考虑太多。而且还很方便。其自带的java客户端简单且方便使用。

1,安装tigerVNC的deb包。

2,vncserver :1 -depth 24 -geometry 1380×900 (随意设置大小)

3, 如果使用的是Ubuntu 16.04,那么很有可能一进去是没有侧边栏之类显示的。原因是~/.vnc/xstartup的脚本确实需要仔细调试。经过一番测试,使用以下配置没有问题,并且可以使用上unity。

xrdb $HOME/.Xresources
xsetroot -solid grey
x-window-manager &
unity &

其余脚本统统多余。。

X11-Forwarding

如果没有对桌面有很强烈的美感需求,使用x11-forwarding真的是一个很好的选择。在Mobamx这种终端工具下,直接敲命令行可以使用托管的图形界面。

1,配置/etc/ssh/sshd_config下的

X11Forwarding yes
X11DisplayOffset 10
X11UseLocalhost yes

2,使能Putty的 Connection/SSH/X11/Enable X11 forwarding。

3,在X display location填上:localhost:10(sshd_config里的默认偏移号)。

4, 使用xming或者mobaXterm直接输入带GUI程序的二进制。

使用x11-forwarding过程中发现,除了Visual Studio Code的菜单显示有问题外。其它的GUI程序基本正常,甚至Android Studio的调试模拟器也能很好的显示出来。我认为基本能够满足日常需要。

总结:

1, 对x11的启动脚本要有一些认识,要了解xstartup每行脚本的具体作用。

2,realvnc的老版本不要使用,非常不方便。可能收费版本会好些。(我用的是一个老的有license的版本)。

3,客户端是通用的,用realvnc的客户端也可以访问tigervnc的server。因为遵循一样的通信协议(Virtual Network Computing)。

4,网上的一些声音:xfce桌面会快一些,容易配置一些。

5,x11-forwarding基本够用。颜值党上vnc。

xamarin的一点调研

跨平台开发一直很fancy。实际情况是有经验的开发者通常能够猜到他们需要付出的代价。

xamarin声称自己能够复用几乎所有的代码。这意味着ios上写的代码逻辑可以复用到android和windows phone上,听起来有点不可思议。怎么做到的呢?

答案就是提供调用本地库, 第三方库的能力。

1,但xamarin要适配最新的os,一定要会比native开发更慢,它提供的sdk要去适配新的native api。

2,而且中间肯定有适配层,如果一个跨平台的小白直接使用这种能力,就意味着做出来的应用效率更差。

3,如果对性能有要求,就必须同时设计开发两个平台的应用程序。考虑到复用逻辑的效率,库的使用是否满足跨平台的要求。

4,如果只是有一个android程序,想迁移到ios版本,重新使用xamarin封装。那基本就是从头再写。从网上的反应看来,代价不少。

综上所述:使用visual studio for mac 开发跨平台的应用,更适合那些已经熟练使用C#的开发者。让这些开发者快速搭建原型,享受一次开发多平台部署的快感。毕竟这种感觉,目前只有xamarin才能带给你。

Anders Hejlsberg解析现代compiler

Anders Hejlsberg on modern compiler architecture

Anders大神曾是TurboPascal作者以及Delphi的首席架构师,后来在微软负责Visual J++和MFC的架构。再后来成为C#和.NET的主要设计师和Typescript的核心贡献者。这个视频是他在channel9对现代编译器的一个white board talker。我只略懂一些编译原理的知识。总结下:现代的编译器基于language server, 修改即解析,复用sytax tree。多了很多Json的消息交互,对机器的性能要求更高。

C++的 { } 和 ( )

{ } 和 ( ) 是C++两大初始化利器。也很让人迷惑。

( ) 表示函数调用,包括普通函数和构造函数。同时也可以给内建类型初始化。

因为构造函数和初始化经常联系起来,( )初始化看起来更像一种传统做法。而且在C++标准语法中,圆括号也是和new配套的初始化语法。见 C++标准 5.3.4 第 17 小节:

A new-expression that creates an object of type T initializes 
that object as follows:
 (17.1) — If the new-initializer is omitted, the object is 
default-initialized (8.5). 
[ Note: If no initialization is performed, the object has an indeterminate value. 
 — end note ]
 (17.2) — Otherwise, the new-initializer is interpreted 
 according to the initialization rules of 8.5 for   
 direct initialization.
 int* p = new int[1000];    //虽然大多数情况下数组元素为0. 
                            //编译器从来为提供初始化保证。
 int* p = new int[1000]();  //内建类型圆括号初始化是语法,是标准。
 int* p = new int[1000]{};  //Scott Meryer推荐现代C++使用花括号。

{} 提供列表初始化的功能,最终调用的是构造函数。比如

  • Point{ } 调用的是Point::Point()构造函数。而
  • Point{x,y} 调用的是Point::Point(x,y)构造函数。

可以看出,{ } 以初始化列表的方式explicit的调用了构造函数。提供了除了New,Operator New, placement New以外的创造新类的方法。

构造出一个匿名的类直接使用 Point{}即可,它调用构造函数,但是临时的,没有名字来指向它。所以调用完再也无法找到这个匿名类。这种适用于工具类函数,调用下出个结果就撤,给他一个类的名字也没多大用。比如:

为了使用类的这种匿名功能,通常标准库会这样玩,比如<utility>头文件中的std::hash就可以这样使用:

std::hash<std::string>{}(“/sensor/percept/lidar_topic”);

它构造一个匿名的std::hash类,然后调用该类的operator(T)方法,得到一个size_t的hash字符串。std::hash<std::string>{} 构造出的就是一个 xvalue ( 右值 ),随后就析构。

TLS协议和HTTP的嵌套

抓了一个TLSv1.2的协议包

协议可以互相嵌套

— ethernet II (SA,DA)

— Internet Protocol(SIP,DIP)

— Transmission Control Protocol( sport,dport,seq,ack,len)

— Hypertext Transfer Protocol( Proxy 443)

— Secure Socket Layer(Application Data Protocol http2, encrypted app data)

— Hypertext Transfer Protocol

— Secure Socket Layer

mojave下c++ functional

如果在Mojave下工作,可以发现Mac的可选的编译器种类比较多:

简单来说,如果使用了brew+安装了xcode。至少有以下几个编译器:

1, gcc-5/g++-5 : 不支持functional

2, x86_64-apple-darwin14.3.0-g++6 / x86_64-apple-darwin14.3.0-gcc-6:正常使用。

3, clang++/clang: 版本10.0.1, 不支持std::bind

测试代码如下:

#include <iostream>
#include <functional>

/*---- c function pointer ------*/
typedef  int (*myfunc)(int a,int b);

int abc(int a, int b )
{
  std::cout<<"i am original fp: "<< a << " " << b<<std::endl;
  return 0;
}

/*----class function ------*/
class test{
public: 
   test(int mem):member(mem){};
    ~test(){};
  int m_func(int a, int b)
  {
     std::cout<<"member function:  " << a << "  " << b<<std::endl; return 0;
  };
 private:
    int member;
};


int main()
{
    // use class instance member to init a std::function
    test ins(2);
    std::function<int(int,int)> a = std::bind(&test::m_func,&ins,std::placeholders::_1,std::placeholders::_2);
    a(1,2);
    
    // use raw c function pointer to init a std::function
    std::function<int(int,int)> b(abc);
    b(3,3);

    // use a lamda expr. to init a std::function
    std::function<int(int,int)> c( [](int a, int b)->int{ std::cout<<"lamda oks"<<std::endl; return 0;} );
    c(4,4);

    /* amazing, you can get raw function point from std::function
      but this not recommended by ISO c++ */
    myfunc* d = b.target<myfunc>();
    (*d)(1,2);
    std::cout<< "function address:  " << d<<std::endl;
    std::cout<<b.target_type().name()<<"  ==vs=== " <<typeid(myfunc).name()<<std::endl;
   
    // but this will cause a segmentation fault !    
    myfunc* e = a.target<myfunc>();
    //(*e)(666,666);
    std::cout<< e<<std::endl;
    std::cout<<a.target_type().name()<<"  ==vs=== " <<typeid(myfunc).name()<<std::endl;
    
    // but this will cause a segmentation fault ! 
    myfunc* f = c.target<myfunc>();
    std::cout<< f<<std::endl;
    std::cout<<c.target_type().name()<<"  ==vs=== " <<typeid(myfunc).name()<<std::endl;
     
    //(*f)(999,999);
}

输出如下:

member function: 1 2
i am original fp: 3 3
lamda oks
i am original fp: 1 2
function address: 0x7b52c2300e80
PFiiiE ==vs=== PFiiiE
0
St5_BindIFSt7_Mem_fnIM4testFiiiEEPS1_St12_PlaceholderILi1EES6_ILi2EEEE ==vs=== PFiiiE
0
Z4mainEUliiE_ ==vs=== PFiiiE

总结:

1,std::function 友好的封装了函数指针,适用于函数回调的场景。

2,内部使用了多态,同时🈶很好的类型检查。如果在嵌入式场景下使用,引入的库带来的负担较重,需要仔细斟酌。

3,成员函数和Lambda表达式不能通过target获取指针,原因是不知道该函数类型。这意味着不能随意的把一个成员函数指针和Lamda变成一个C函数指针。这会给C++库赋值给C库的回调函数带来一定的使用限制。

4,静态函数和普通全局函数可以很方便的和std::function转换。