从auto_ptr到unique_ptr:浅谈C++右值引用、移动语义与智能指针
std::auto_ptr是C03对智能指针的第一次尝试,作为一个失败品,其甚至已然在后续的标准中被移除,但时至今日,我们依然可以透过它一窥C发展史的一角。
出于方便、严谨起见,下文所提及类与函数,如未特别标明命名空间,均为std或其子命名空间下的标准库设施。
std::auto_ptr的失败之处
auto_ptr在语义上是有些类似它的后辈unique_ptr的,其拷贝构造/赋值函数并非深拷贝或浅拷贝,而是被设计成了资产的所有权转移即move语义,从而保证一份资源同时只能被一根auto_ptr所持有:
1 | template <class _Ty> |
可以看到,auto_ptr的copy ctor实际上执行的是C++11及以后的move ctor的工作,为此,其参数特意取消了const限定。这看似是个好设计,然而却有着致命的缺陷。考虑以下场景:
1 | std::vector<std::auto_ptr<int>> aptrVec; |
或许难以想象,但这段理所应当的代码是实实在在的编译不过的。其原因是push_back时会发生拷贝构造的,而push_back的参数为const _Ty& _Val,在发生拷贝构造时const限定的参数自然无法传入非const限定的拷贝构造函数中,从而编译失败。
堂堂智能指针,竟然无法放到任何一个容器中去,何其荒诞可笑。更何况除此以外,auto_ptr依旧存在很多缺陷——这里不一一赘述,总之,在十多年前的那会儿,C++急需一套完备的智能指针来替换掉auto_ptr这个笑话。
std::unique_ptr如何成功?
C标准委员会说要有unique_ptr,于是就有了unique_ptr。
C标准委员会说unique_ptr是好的,于是……于是他们就忘了在C++11里加上make_unique了 😃
前面说到,auto_ptr想用copy ctor实现move语义,但却与容器库冲突,惨遭失败。想要一雪前耻,那么就需要C++11的重量级特性右值引用了。
虽然我们常说,右值引用的作用是“延长右值生命周期”,即:
1 | Type&& t = Type{}; |
但实际上右值引用更为重要的作用是区分copy与move语义,而这一点又着重体现在C++11中由过去三法则演变而来的五法则上:
1 | class Fuxk |
相比与C11之前的三法则dtor、copy ctor、copy op=,C11新增了以右值引用作为参数的move ctor和move op=,正式将copy语义与move语义分隔开,使得用户可以更加自由的对内存所有权进行管理。
而这一切最直接的受益者之一,就是std::unique_ptr了。
前面我们说,auto_ptr的目标是既要“保证一份资源同时只能被一根auto_ptr所持有”又要“支持资产所有权的转移”,这是只依靠copy语义无法完成的,而在C++11中,通过copy move语义的拆分,我们可以很自然而然的delete掉copy ctor/op=来保证所有权的唯一性,同时提供move ctor/op=来支持所有权的转移。
我并没有精力去搜寻资料与论文去考据当年右值引用被提出的理由,但站在现在的视角来看,经受住了时间的考验、成功的智能指针系统毫无疑问是建立在右值引用存在的基础之上的,其二者有着千丝万缕的联系,他们共同构成了Modern C++的基石。
这篇文章并非什么语法的辨析与讲解,讨论的也是十年前C11的一些老东西,单纯是我在学习与思考C发展过程中的一些发散。
一直以来大多数讲述Modern C特性的教程/文章都会将智能指针与右值引用/移动语义/完美转发分成两个单独的part来讲解、而忽略其它们之间的联系,同时又几乎不约而同地忽略了失败品auto_ptr,使初学者难以领会到C十几年间的演化历程,于是经常会看到新人开发者在社区中提问——“C++为什么要有XXX?”
故此就有了这篇不成气候的随笔,希望能够给予初学者们一些思路与帮助。
