[Cpp基础] [02] 拷贝/移动与引用

这一部分主要介绍左值,右值,引用,拷贝和移动.这些可以说是C++11显著区别于以前的C++(还有C)的特性.本文主要从易用的角度介绍值类型(值类型实际要更多一些).

拷贝与移动的基本定义

  • "拷贝"和"移动"是从逻辑层面定义的,在最终的指令层面,只有"拷贝",没有"移动"操作.
  • 对于自定义类,拷贝与移动的实现有基本的准则(非硬性),这些准则是:
    • 拷贝:不应对源对象做任何修改.
    • 移动:可以对源对象做修改,但是源对象被修改后必须可以被赋值,且可以安全的析构.
  • 注意,对于移动操作.我们不能对移出后剩余的部分做任何假定,在没有重新赋值前,不应该使用移出后剩下的对象
    • 例如:对一个vector<int> a;,其中的所有元素都被auto b = std::move(a);移出后,不能假定a.size()值为0了.
  • 一般而言:
    • 对于同时支持拷贝和移动的类型,我们一般认为,移动操作的性能开销应该优于拷贝.
    • 对于锁,线程等逻辑上唯一的资源,其拷贝没有意义,只能进行移动.例如,锁对象之间的移动是在转交锁的所有权.
    • 对于某个类型,如果拷贝的性能足够好(满足需求),那么应该优先使用拷贝.
  • 一个常见的风格: 所有类型都应该是copy-only或move-only的,必要时,可以给move-only的类型添加一个clone().
    • 使用这种风格编程的主要优势是可以降低心智负担,进而降低维护成本.
    • 这种风格认为,如果类型支持拷贝,那么拷贝一定是高效的,否则就应该让它变成move-only.这样一来,只要编译器不报错,那么代码的基本性能就有保障.

左值,右值

  • 语言层面的"对象"可以是左值/右值其中一种.
  • 从形式上说, 如果某个"对象"只在一个expression中有意义,那么它就是右值,如临时量,字面值常量.
  • 左值意味更加持久的生命周期, 右值的生命周期是完全由编译器管理的.

左值引用T&与右值引用T&&

  • 将引用理解为特殊的指针,对于理解引用的行为很有帮助.(大部分编译器的底层实现就是常量指针)
    • 将对象绑定到引用时,就是自动取地址并赋值给指针.
    • 通过引用访问对象时,会自动的对指针加解引用符号.
    • 除此之外,编译器不会做其他操作.
  • 大部分时候,当你遇到困惑时,都可以通过将引用换为指针的方式来辅助自己进行分析.例如
    • 绑定仅是取地址,所以绑定操作不会触发任何的构造函数
    • 引用销毁时只是指针的销毁,不会触发绑定对象的析构函数.
    • 将ref又绑定到ref2上,就是指针的拷贝,自然也不会涉及对象的拷贝(移动).
    • 左值引用,右值引用是"类型不同的"的指针.
    • 以引用形式进行函数传参(返回)时,就是在传入(返回)指针.
  • 左值只可能绑定到左值引用,即{T &,const T &}.
  • 右值只可能绑定到右值引用或const T &,即{T &&,const T &&,const T &}.
    • 注意,右值绑定到const T &是一个cast, 并不是精确匹配,也就是说它可能触发隐式类型转换, 例如const Foo & a = Bar(),等价于const Foo & a = Foo(Bar()),只要Foo支持从Bar移动构造即可.
  • 引用是一种类型,但是C++限制引用必须被初始化,且不能重复绑定,且不能放在容器中.(主要是历史包袱)
    • 必须放在容器中时,使用指针会更合理.
  • 在涉及模板时, 存在"引用折叠"的特性, 因为推导出的类型T可以是引用类型, 这就可能和已有的引用重复.
    • 含有&的都折叠为&,例如MyT && &,MyT & &&,MyT & &,其整体上的类型和MyT&等价;
    • MyT && &&折叠为MyT&&
  • 注意:引用只能绑定到在编译期长度已知的数组,
    • void f(int(&r)[]);//误,必须有长度
    • void f(int(&r)[100]);//正
    • Type array[N]; auto &ref = array;//正,编译器可推断长度
    • Type array[100]; auto (&ref)[100] = array;//正,手动告知长度
    • 在模板中我们可以利用引用推断数组的长度,template<class T,int N> void f(T(&r)[N]);//由编译器来推断长度

引用作为形参/返回值

  • 引用主要是作为函数的形参或返回值类型, 在常规代码中极少用到引用.

    • 常规代码中,常常用引用来给对象起别名,这可能会让代码更好看,例如auto & point_info = *p_full_point_information;,不但可以让名字变短,还能避免在后续的代码中使用->.
  • 在函数重载中的规则如下.

    • 右值将精确匹配到T&&const T&&形参的函数。当这两个函数未定义时,才可能重载到cosnt T &
    • 左值对象只能匹配到T&const T&
    • 左值对象和右值对象一般都能初始化T形参,T形参和引用形参优先级相同.
  • 虽然逻辑上T&&,const T&&,T&,const T&都是合法的类型,在实践中中,我们只应该使用const T&T&&.

    • T&总是应该由T *替代,强调对象可能被修改.
  • 从逻辑上说,T&&T&形参传入的对象允许被修改.

    • 一般而言,我们会认为T&&型传入的对象一定会被修改,且仅保证修改后的对象仅仅可以安全的析构.
    • T&型传入的对象是否被修改并不确定
    • 从编程风格上说,如果传入对象不会被修改,就应该用const T&,使用T&&T&意味着传入对象一定会被修改.
  • const T&const T&&传入的对象不能修改

    • const T&&仅在成员重载this等极其冷门的场合中使用,在没有明确的理由时,应优先使用const T&
  • 作为函数的返回形参时,习惯上我们常使用const T&T&

    • 返回const T&&T&&的也是理论上可行的,一般只用在模板中.

引用相关的static_cast

  • 通过static_cast<T&&>可以将对象视作右值来处理.
    • 例如 static_cast<int &&>(a) = 10;就会触发编译错误,等号左侧是一个右值.
    • 注:xxx_cast<yy>(obj)要求obj只能是左值表达式
  • std::move(obj)表达式的效果和static_cast<T&&>(obj)一致,实现上使用了模板.

引用与拷贝控制

有了之前的背景作为铺垫,我们就可以详细的介绍C++中的拷贝和移动了.

  • 对于自定义类T
    • T(const T&)为拷贝构造,T& operator=(const T&)为拷贝赋值.
    • T(T&&)为移动构造,T& operator=(T&&)为移动赋值.
  • 因为移动构造函数同时会修改源对象和目标对象,如果因异常中断,移动过程就可能无法恢复现场(导致异常不安全).基于此,标准库在vector等容器中,只当容器元素的移动控制函数为noexcept时才会使用移动.

Read more

Coroutine

从一般概念上说, 协程是特殊的函数调用: 被调用的函数可以在可控的位置被中断,然后在下一次调用时,继续从上次中断的位置继续执行。 本文主要通过Python的协程来介绍协程, 这是我唯一熟悉的一种协程实现. Classic Coroutine 下面的python代码很好的说明了协程的核心功能 def co_routine(): recv0 = yield 996 # hangs here after first coro.send assert recv0 == "Second" yield 711 # hangs here after second coro.send return def main(): coro = co_routine() # Create a new coroutine object value = coro.send(None)

By Edimetia3D

GDB with Python

这篇文章的主要应用场景是调试Python的C/C++ Extension 1. 同时使用pdb / gdb 进行调试. 通俗点说, 既可以break在 .py 文件中,也可以break在 .cc 文件中 2. 在gdb中不但可以获得常规的调试信息, 还可以获得python VM 的调试信息, 例如获得python的调用栈, 访问Python局部变量等. 这将会在调试exception时(如Segmentfalut)非常有用, 这种场景下, 定位 Python VM 正运行到哪一行代码往往可以提供一些直观的重要信息. 第一步: 编译源码以获得一些辅助数据. 我们并不真的需要使用从源码编译的Python, 但是一些调试相关的辅助文件需要从源码中获得, 包括 python-gdb.py及debug symbol等. 在 https://www.python.org/ftp/python/ 或 https://github.com/python/cpython

By Edimetia3D

Bazel Notes

这是一篇2019年左右的记录, 内容可能过时, 也不太全面 杂谈 Bazel是Google为Monorepo服务而开发的构建工具. 首先是巨大,当问题的规模变大,事情总是会变得更复杂. 而Google面对的"巨大Monorepo",应该是世间罕有的. 然后是Monorepo,这极大的影响了代码的组织风格.例如,你要写一个操作系统内核ProjectOS,还要写一个游戏ProjectGame.在传统的开发习惯中,这两个项目会组织到两个不同的Repo里,PorjectOS和ProjectGame之间无法直接相互引用,例如,你在ProjectOS里写了一个高级的数据结构,想要在Game里也使用,要么直接复制粘贴,要么是创建一个新的CommonRepo,把可公用的代码都放在Common里,然后两个项目各自引入Common作为依赖. 使用MonoRepo则不存在这个问题,Game可以直接依赖OS内的组件,按照Bazel的语法描述,就是在Game中可以直接使用@ProjectOS//path/to/package:AdvancedStruct.当然,你仍然可以选择重构一

By Edimetia3D

Unix related things

这是一篇2017年左右的记录, 仅用作分享 杂 * 在shell内能干的事,我们都可以比较简单地通过系统调用实现. * `称为反引号,^称为脱字符,常用来表示CTRL * windows的系统调用是不开放的,windows下只能直接使用windows.h里的windows API. * /dev目录下的设备是供用于程序直接使用的,主要由block,char,pipe,socket类型 * 并不是所有设备都能映射为这种形式 * /sys/device/目录称为sysfs,他下面存放了所有设备的信息.(不能直接从/dev获得任何设备信息) * udevadm info --query=all --name="/dev/sda1"可以用于查询/dev下某个设备对应的sysfs路径 权限系统 * 权限系统由两部分组成 * 文件属性:用于标注文件owner,所属组,以及权限的设定(默认只有owner和root可以修改权限设置) *

By Edimetia3D