在C ++中,程序员必须决定如何释放使用的资源;没有像垃圾收集器这样的自动工具。 本文讨论了该问题的可能解决方案,详细研究了潜在问题以及许多相关问题。
目录
引言
资源管理是C ++程序员必须始终执行的工作。 资源包括内存块,OS内核对象,多线程锁,网络连接,数据库连接,以及在动态内存中创建的任何对象。 对资源的访问是通过描述符进行的,描述符的类型通常是指针或其别名之一( HANDLE
等),有时甚至是整个别名(UNIX文件描述符)。 使用资源后,必须释放它,否则迟早不释放资源的应用程序(可能还有其他应用程序)将耗尽资源。 这个问题非常严重,可以说.NET,Java和其他几个平台的关键功能之一是基于垃圾回收的统一资源管理系统。
C ++的面向对象功能自然导致以下解决方案:管理资源的类包含资源描述符作为成员,在捕获资源时初始化描述符,并在析构函数中释放资源。 但是经过一番思考(或经验)之后,人们才意识到它并不是那么简单。 而主要的问题是复制的语义。 如果管理资源的类使用默认编译器生成的复制构造函数,则在复制对象后,我们将获得同一资源句柄的两个副本。 如果一个对象释放了资源,那么第二个对象将能够尝试使用或释放已经释放的资源,这在任何情况下都是不正确的,并且可能导致所谓的未定义行为,即可能发生任何事情,例如程序异常终止。
幸运的是,在C ++中,程序员可以通过自己定义复制构造函数和复制赋值运算符来完全控制复制过程,这使我们通常可以以多种方式解决上述问题。 复制的实现应与释放资源的机制紧密联系,我们将其统称为复制所有权策略。 所谓的“三巨头规则”是众所周知的,它指出,如果程序员定义了三个操作(复制构造函数,复制赋值运算符或析构函数)中的至少一个,则他必须定义所有三个操作。 复制所有权策略仅指定如何执行此操作。 有四种基本的副本所有权策略。
1.基本的副本所有权策略
在捕获资源之前或之后,描述符必须具有一个特殊值,该值指示该资源与资源无关。 通常为零,有时为-1,强制转换为描述符类型。 无论如何,这样的描述符将被称为零。 在这种情况下,管理资源的类必须识别空描述符,并且不要尝试使用或释放资源。
1.1。 禁止复制策略
这是最简单的策略。 在这种情况下,完全禁止复制和分配类实例。 析构函数释放捕获的资源。 在C ++中,要禁止复制并不困难,该类必须声明但不定义封闭的复制构造函数和复制赋值运算符。
class X { private: X(const X&); X& operator=(const X&);
编译和链接器阻止了复制尝试。
C ++ 11标准针对这种情况提供了一种特殊的语法:
class X { public: X(const X&) = delete; X& operator=(const X&) = delete;
尝试复制时,此语法更直观,并向编译器提供更多可理解的消息。
在标准库的早期版本(C ++ 98)中,输入/输出流的类( std::fstream
等)使用了复制禁止策略,而在Windows上,则使用了MFC中的许多类( CFile
, CEvent
, CMutex
等)。 在C ++ 11标准库中,某些类使用此策略来支持多线程同步。
1.2。 独家所有权策略
在这种情况下,实现复制和分配时,资源描述符从源对象移动到目标对象,也就是说,它保留在单个副本中。 复制或分配后,源对象具有空描述符,并且无法使用资源。 析构函数释放捕获的资源。 此策略也使用专有或严格所有权[Josuttis]; Andrei Alexandrescu使用破坏性复制一词。 在C ++ 11中,此操作如下进行:以上述方式禁止常规复制和复制分配,并实现了移动语义,即,定义了移动构造函数和移动分配运算符。 (稍后会详细介绍运动的语义。)
class X { public: X(const X&) = delete; X& operator=(const X&) = delete; X(X&& src) noexcept; X& operator=(X&& src) noexcept;
因此,排他性所有权策略可以被视为对复制禁止策略的扩展。
在C ++ 11标准库中,此策略使用智能指针std::unique_ptr<>
和其他一些类,例如: std::thread
, std::unique_lock<>
以及以前使用过复制禁止策略的类( std::fstream
等)。 在Windows上,以前使用了复制禁止策略的MFC类也开始使用排他所有权策略( CFile
, CEvent
, CMutex
等)。
1.3。 深层复制策略
在这种情况下,您可以复制和分配类实例。 必须定义复制构造函数和复制赋值运算符,以便目标对象将资源从源对象复制到自身。 之后,每个对象都拥有其资源副本,可以独立使用,修改和释放资源。 析构函数释放捕获的资源。 有时,对于使用深度复制策略的对象,使用值对象。
此策略不适用于所有资源。 可以将其应用于与内存缓冲区关联的资源(例如字符串),但是还不清楚如何将其应用于OS内核的对象(例如文件,互斥对象等)。
深度复制策略可用于所有类型的对象字符串, std::vector<>
和标准库的其他容器中。
1.4。 共同所有权策略
在这种情况下,您可以复制和分配类实例。 您必须定义复制构造函数和复制赋值运算符,在其中复制资源描述符(以及其他数据),但不能复制资源本身。 之后,每个对象都有其自己的描述符副本,可以使用,修改但不能释放资源,只要至少有一个拥有描述符副本的对象即可。 拥有句柄副本的最后一个对象超出范围后,将释放资源。 下面描述如何实现。
共同所有权策略通常由智能指针使用,并且将它们用于不可变资源也是很自然的。 std::shared_ptr<>
智能指针在C ++ 11标准库中实现了此策略。
2.深度复制策略-问题与解决方案
考虑一个模板,用于C ++ 98标准库中类型T
的对象的状态交换功能。
template<typename T> void swap(T& a, T& b) { T tmp(a); a = b; b = tmp; }
如果类型T
拥有资源并使用深度复制策略,则我们有三个操作来分配新资源,三个复制操作和三个操作来释放资源。 尽管在大多数情况下可以执行此操作而无需分配新资源和完全不进行复制,但是对象交换内部数据(包括资源描述符)就足够了。 当您必须创建资源的临时副本并立即释放它们时,有许多类似的示例。 日常操作的这种无效实施刺激了对解决方案进行优化的寻找。 让我们考虑主要选项。
2.1。 复制记录
写时复制(COW)也称为延迟复制,可以看作是尝试结合使用深层复制策略和共享所有权策略。 最初,在复制对象时,将复制资源描述符,而没有资源本身,并且对于所有者,资源变为共享和只读的,但是一旦某些所有者需要修改共享资源,便会立即复制资源,然后该所有者使用其资源副本。 实施COW解决了状态交换的问题:不会发生额外的资源分配和复制。 在实现字符串时,非常流行使用COW;例如, CString
(MFC,ATL)。 在[Meyers1],[Sutter]中可以找到实施COW和新兴问题的可能方法的讨论。 [Guntheroth]提出了使用std::shared_ptr<>
的COW实现。 在多线程环境中实现COW时会出现问题,这就是为什么在标准C ++ 11库中禁止对字符串使用COW的原因,请参见[Josuttis],[Guntheroth]。
COW想法的发展导致以下资源管理方案:资源是不可变的,并使用共享所有权策略由对象管理,如有必要,更改资源,创建新的,经过适当修改的资源,并返回新的所有者对象。 此架构用于.NET和Java平台上的字符串和其他不可变对象。 在函数式编程中,它用于更复杂的数据结构。
2.2。 定义类的状态交换函数
上面显示了通过复制和分配以简单的方式实现状态交换功能的效率如何低下。 而且它使用非常广泛,例如,它被标准库的许多算法所使用。 为了使算法不使用另一个std::swap()
,而是使用专门为该类定义的另一个函数,必须执行两个步骤。
1.在类中定义实现状态Swap()
的成员函数Swap()
(名称不重要)。
class X { public: void Swap(X& other) noexcept;
您必须确保该函数不会引发异常;在C ++ 11中,此类函数必须声明为noexcept
。
2.在与类X
相同的名称空间中(通常在同一头文件中),如下定义自由(非成员) swap()
函数(名称和签名是基本的):
inline void swap(X& a, X& b) noexcept { a.Swap(b); }
之后,标准库的算法将使用它,而不是std::swap()
。 这提供了一种称为自变量依赖查找(ADL)的机制。 有关ADL的更多信息,请参见[Dewhurst1]。
在C ++标准库中,所有容器,智能指针以及其他类都实现了如上所述的状态交换功能。
Swap()
成员函数通常很容易定义:如果数据库和成员支持,则必须顺序地对数据库和成员应用状态交换操作,否则,必须对std::swap()
进行状态交换操作。
上面的描述有些简化,可以在[Meyers2]中找到更详细的描述。 关于状态交换功能的问题的讨论也可以在[Sutter / Alexandrescu]中找到。
状态交换功能可以归因于该类的基本操作之一。 使用它,您可以优雅地定义其他操作。 例如,复制分配运算符通过copy和Swap()
定义如下:
X& X::operator=(const X& src) { X tmp(src); Swap(tmp); return *this; }
此模板称为复制和交换惯用语或Herb Sutter惯用语,有关更多详细信息,请参见[Sutter],[Sutter / Alexandrescu],[Meyers2]。 它的修改可以应用于实现位移的语义,请参见第2.4、2.6.1节。
2.3。 编译器删除中间副本
考虑上课
class X { public: X();
和功能
X Foo() {
通过简单的方法,通过复制X
的实例来实现Foo()
函数的返回X
但是编译器能够从代码中删除复制操作,直接在调用点创建对象。 这称为返回值优化(RVO)。 RVO已被编译器开发人员使用了相当长的一段时间,目前已在C ++ 11标准中得到修复。 尽管有关RVO的决定是由编译器决定的,但程序员仍可以根据其用途编写代码。 为此,希望函数具有一个返回点,并且返回表达式的类型与函数的返回值的类型匹配。 在某些情况下,建议定义一个特殊的封闭构造函数,称为“计算构造函数”,有关更多详细信息,请参见[Dewhurst2]。 在[Meyers3]和[Guntheroth]中也讨论了RVO。
在其他情况下,编译器可以删除中间副本。
2.4。 置换语义的实现
移动语义的实现包括定义一个移动构造函数,该构造函数具有对源的rvalue-reference类型的参数和具有相同参数的移动赋值运算符。
在C ++ 11标准库中,状态交换功能模板定义如下:
template<typename T> void swap(T& a, T& b) { T tmp(std::move(a)); a = std::move(b); b = std::move(tmp); }
根据解决具有右值引用类型参数的函数重载的规则(请参见附录A),如果类型T
具有移动构造函数和移动赋值运算符,则将使用它们,并且不会分配临时资源和进行复制。 否则,将使用复制构造函数和复制分配运算符。
使用重定位的语义避免了在比上述状态交换功能更广泛的上下文中创建临时副本。 运动语义适用于任何右值,即临时的未命名值,以及函数的返回值(如果该函数是在本地创建的(包括左值)且未应用RVO)。 在所有这些情况下,都可以保证在移动之后不能以任何方式使用源对象。 move语义也适用于将std::move()
转换应用到的左值。 但是在这种情况下,程序员负责移动之后如何使用源对象(示例std::swap()
)。
考虑到运动的语义,对标准的C ++ 11库进行了重新设计。 许多类都添加了带有rvalue类型引用参数的move构造函数和move赋值运算符以及其他成员函数。 例如, std::vector<T>
具有void push_back(T&& src)
的重载版本。 所有这些在许多情况下都可以避免创建临时副本。
实现移动语义不会取消类的状态交换功能的定义。 专门定义的状态交换函数比标准std::swap()
更有效。 而且,使用以下状态交换的成员函数(复制和交换惯用语的变体)非常容易地定义move构造函数和move赋值运算符:
class X { public: X() noexcept {} void Swap(X& other) noexcept {} X(X&& src) noexcept : X() { Swap(src); } X& operator=(X&& src) noexcept { X tmp(std::move(src));
move构造函数和move赋值运算符是那些成员函数,非常需要确保它们不引发异常,并因此将其声明为noexcept
。 这使您可以优化标准库容器的某些操作,而不会违反对异常安全性的严格保证;有关更多详细信息,请参见[Meyers3]和[Guntheroth]。 提议的模板提供了这样的保证,条件是默认构造函数和状态交换的成员函数不引发异常。
C ++ 11标准为编译器提供了自动生成移动构造函数和移动赋值运算符的方法,为此,必须使用"=default"
构造进行声明。
class X { public: X(X&&) = default; X& operator=(X&&) = default;
通过将移动操作顺序地应用于类的基类和成员(如果它们支持移动),然后复制操作,来实现这些操作。 显然,这种选择远非总是可以接受的。 原始描述符不会移动,但是通常无法复制它们。 在某些条件下,编译器可以独立生成类似的移动构造函数和移动赋值运算符,但最好不要利用此机会,这些条件很容易混淆,并且在精炼类时很容易更改。 有关详细信息,请参见[Meyers3]。
通常,位移语义的实现和使用是一件“轻巧的事情”。 编译器可以在程序员希望移动的地方应用复制。 这里有一些规则可以消除或至少减少这种情况的可能性。
- 如果可能,请使用禁止复制。
- 声明move构造函数并将move赋值运算符声明为
noexcept
。 - 为基类和成员实现运动语义。
- 将
std::move()
转换应用于右值引用类型的函数的参数。
上面讨论了规则2。规则4是由于命名的右值链接是左值的事实(另请参见附录A)。这可以通过移动构造函数的定义示例来说明。
class B {
在定义移动分配运算符时,上面给出了此规则的另一个示例。在6.2.1节中还将讨论位移语义的实现。
2.5。住宿与 插入
, RVO (. 2.3), , . ( ), , . , . C++11 - emplace()
, emplace_front()
, emplace_back()
, . , - — (variadic templates), . , C++11 — .
:
- , , .
- , , .
, .
std::vector<std::string> vs; vs.push_back(std::string(3, 'X'));
std::string
, . . , , . , [Meyers3].
2.6。 总结
, , . - . . — : , . , , , . : , , «» .
: , , .NET Java. , Clone()
Duplicate()
.
- - , :
- .
- .
- - rvalue-.
.NET Java - , , .NET IClonable
. , .
3.
, . - , . , . Windows: , HANDLE
, COM-. DuplicateHandle()
, CloseHandle()
. COM- - IUnknown::AddRef()
IUnknown::Release()
. ATL ComPtr<>
, COM- . UNIX, C, _dup()
, .
C++11 std::shared_ptr<>
. , , , , , . , . std::shared_ptr<>
[Josuttis], [Meyers3].
: - , ( ). ( ) , . std::shared_ptr<>
std::weak_ptr<>
. . [Josuttis], [Meyers3].
- [Alexandrescu]. ( ) , [Schildt]. , .
( ) [Alger].
-. [Josuttis] [Alexandrescu].
- .NET Java. , , , .
4.
, C++ rvalue- . C++98 std::auto_ptr<>
, , , . , , ( ). C++11 rvalue- , , . C++11 std::auto_ptr><>
std::unique_ptr<>
. , [Josuttis], [Meyers3].
: - ( std::fstream
, etc.), ( std::thread
, std::unique_lock<>
, etc.). MFC , ( CFile
, CEvent
, CMutex
, etc.).
5. —
. , . , , , . , , , ( ) . , , , . ( ) , . , . — . 6.
, - -, « », - . - . , , , , - . «».
6. -
, - . , -. .
6.1.
- . , , :
- . , .
- .
- .
, , , . C++11 .
« » (resource acquisition is initialization, RAII). RAII ( ), ., [Dewhurst1]. «» RAII. , , , (immutable) RAII.
6.2.
, RAII, , , . - , , - . , , , . .
6.2.1.
, , , , :
- , .
- .
- .
- .
C++11 , , , . , - clear()
, , , . . , shrink_to_fit()
, , (. ).
, RAII, , , . , .
class X { public:
.
X x;
std::thread
.
2.4, - . , - - . .
class X {
:
X::X(X&& src) noexcept : X() { Swap(src); } X& X::operator=(X&& src) noexcept { X tmp(std::move(src));
- :
void X::Create() { X tmp();
, , , - . , , , . , .
- « », , . : , , ( ). : , . , : , , . , . [Sutter], [Sutter/Alexandrescu], [Meyers2].
, RAII .
6.2.2.
RAII . , , , , :
- , .
- .
- . , .
- .
- .
«» RAII, — . , , . 3. . «», .
6.2.3.
— . RAII , . , . , , ( -). - ( -). 6.2.1, .
6.3.
, - RAII, : . , , .
7.
, , , , . - -.
4 -:
- .
- .
- .
- .
. , - : , , - .
, . , , -, , .
- . . , (. 6.2.3). , (. 6.2.1). , . , , . , std::shared_ptr<>
.
应用领域
. Rvalue-
Rvalue- C++ , , rvalue-. rvalue- T
T&&
.
:
class Int { int m_Value; public: Int(int val) : m_Value(val) {} int Get() const { return m_Value; } void Set(int val) { m_Value = val; } };
, rvalue- .
Int&& r0;
rvalue- ++ , lvalue. 一个例子:
Int i(7); Int&& r1 = i;
rvalue:
Int&& r2 = Int(42);
lvalue rvalue-:
Int&& r4 = static_cast<Int&&>(i);
rvalue- ( ) std::move()
, ( <utility>
).
Rvalue rvalue , .
int&& r5 = 2 * 2;
rvalue- .
Int&& r = 7; std::cout << r.Get() << '\n';
Rvalue- .
Int&& r = 5; Int& x = r;
Rvalue- , . , rvalue-, rvalue .
void Foo(Int&&); Int i(7); Foo(i);
, rvalue rvalue- , . rvalue-.
, , , rvalue-, (ambiguous) rvalue .
void Foo(Int&&); void Foo(const Int&);
Int i(7); Foo(i);
: rvalue- lvalue.
Int&& r = 7; Foo(r);
, rvalue-, lvalue std::move()
. . 2.4.
++11, rvalue- — -. (lvalue/rvalue) this
.
class X { public: X(); void DoIt() &;
.
, ( std::string
, std::vector<>
, etc.) . — . , rvalue- . , , - , - , . , , , rvalue, lvalue. , rvalue. . , ( lvalue), RVO.
参考文献
[Alexandrescu]
, . C++.: . 来自英语 -M .: LLC“ I.D. », 2002.
[Guntheroth]
, . C++. .: . 来自英语 — .: «-», 2017.
[Josuttis]
, . C++: , 2- .: . 来自英语 -M .: LLC“ I.D. », 2014.
[Dewhurst1]
, . C++. , 2- .: . 来自英语 — .: -, 2013.
[Dewhurst2]
, . C++. .: . 来自英语 — .: , 2012.
[Meyers1]
, . C++. 35 .: . 来自英语 — .: , 2000.
[Meyers2]
, . C++. 55 .: . 来自英语 — .: , 2014.
[Meyers3]
, . C++: 42 C++11 C ++14.: . 来自英语 -M .: LLC“ I.D. », 2016.
[Sutter]
, . C++.: . 来自英语 — : «.. », 2015.
[Sutter/Alexandrescu]
, . , . ++.: . 来自英语 -M .: LLC“ I.D. », 2015.
[Schildt]
, . C++.: . 来自英语 — .: -, 2005.
[Alger]
, . C++: .: . 来自英语 — .: « «», 1999.