从auto_ptr到unique_ptr:浅谈C++右值引用、移动语义与智能指针

std::auto_ptr是C++03对智能指针的第一次尝试,作为一个失败品,其甚至已然在后续的标准中被移除,但时至今日,我们依然可以透过它一窥C++发展史的一角。 出于方便、严谨起见,下文所提及类与函数,如未特别标明命名空间,均为std或其子命名空间下的标准库设施。

std::auto_ptr的失败之处

auto_ptr在语义上是有些类似它的后辈unique_ptr的,其拷贝构造/赋值函数并非深拷贝或浅拷贝,而是被设计成了资产的所有权转移即move语义,从而保证一份资源同时只能被一根auto_ptr所持有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <class _Ty>
class auto_ptr
{
public:
auto_ptr(auto_ptr& _Right) noexcept : _Myptr(_Right.release()) {}
_Ty* release() noexcept
{
_Ty* _Tmp = _Myptr;
_Myptr = nullptr;
return _Tmp;
}
private:
_Ty* _Myptr; // the wrapped object pointer
}

可以看到,auto_ptr的copy ctor实际上执行的是C++11及以后的move ctor的工作,为此,其参数特意取消了const限定。这看似是个好设计,然而却有着致命的缺陷。考虑以下场景:

1
2
3
std::vector<std::auto_ptr<int>> aptrVec;
std::auto_ptr<int> aptr(new int(1));
aptrVec.push_back(aptr);

或许难以想象,但这段理所应当的代码是实实在在的编译不过的。其原因是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
2
3
4
5
6
7
8
9
10
class Fuxk
{
public:
Fuxk();
~Fuxk();
Fuxk(const Fuxk&);
Fuxk(Fuxk&&) noexcept;
Fuxk& operator=(const Fuxk&);
Fuxk& operator=(Fuxk&&) noexcept;
};

相比与C++11之前的三法则dtor、copy ctor、copy op=,C++11新增了以右值引用作为参数的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++的基石。 ___ 这篇文章并非什么语法的辨析与讲解,讨论的也是十年前C++11的一些老东西,单纯是我在学习与思考C++发展过程中的一些发散。

一直以来大多数讲述Modern C++特性的教程/文章都会将智能指针与右值引用/移动语义/完美转发分成两个单独的part来讲解、而忽略其它们之间的联系,同时又几乎不约而同地忽略了失败品auto_ptr,使初学者难以领会到C++十几年间的演化历程,于是经常会看到新人开发者在社区中提问——“C++为什么要有XXX?”

故此就有了这篇不成气候的随笔,希望能够给予初学者们一些思路与帮助。

原文链接: https://zhuanlan.zhihu.com/p/551883955