我正在阅读一篇由于名为RAII的文章,您不再需要清理代码。
促使这项研究的原因是我目前正在编写需要在退出函数之前进行清理的内容。
例如,我创建了一个文件,并映射了一个文件的视图。
通常,我只是使用goto
或do {break;} while(false);
退出。但是,C ++ 11不再需要它吗?
即。没有了
if( fail ) {
UnmapViewOfFile(lpMapView);
CloseHandle(hFileMap);
CloseHandle(hFile);
}
每几行代码?
一旦函数退出,编译器会自动将其包装起来吗?它似乎很难相信它实际上清理了文章所说的功能调用。 (我可能会以某种方式误解它。)更可能的是它只是通过从C ++库中调用它们的解构函数来清理创建的类库。
编辑:The article - 来自维基百科:它并不一定声明它会清除这些函数调用,但它确实意味着它对C ++库函数对象(例如FILE *
,fopen
等对象)有效。
它也适用于WinAPI吗?
答案 0 :(得分:7)
C ++标准肯定没有说明使用像UnmapViewOfFile或CloseHandle这样的Windows API函数。 RAII是一种编程习惯用语,你可以使用或不使用它,它比C ++ 11更老。
推荐使用RAII的原因之一是,在处理异常时,它可以让生活更轻松。析构函数将始终安全地释放任何资源 - 主要是内存,但也处理。对于内存,你在标准库中有类,比如unique_ptr和shared_ptr,还有vector和很多其他类。对于像WinAPI那样的句柄,你必须编写自己的句柄,如:
class handle_ptr {
public:
handle_ptr() {
// aquire handle
}
~handle_ptr() {
// release
}
}
答案 1 :(得分:7)
清理仍然是必要的,但由于可能存在异常,代码不应仅通过在函数末尾执行清理操作来进行清理。可能永远无法达到这一目的!取而代之的是,
在析构函数中进行清理。
在C ++ 11中,在析构函数中进行任何类型的清理都非常容易,无需定义自定义类,因为现在更容易定义范围保护类。范围守卫是由Petru Marginean发明的,他与Andrei Alexandrescu在DDJ上发表了一篇关于它的文章。但是原始的C ++ 03实现非常复杂。
在C ++ 11中,一个简单的范围保护类:
class Scope_guard
: public Non_copyable
{
private:
function<void()> f_;
public:
void cancel() { f_ = []{}; }
~Scope_guard()
{ f_(); }
Scope_guard( function<void()> f )
: f_( move( f ) )
{}
};
其中Non_copyable
提供移动分配和移动构造以及默认构造,但使复制分配和复制构造成为私有。
现在,在成功获取某些资源之后,您可以声明一个Scope_guard
对象,该对象将保证在范围结束时进行清理,即使面对异常或其他早期返回,例如
Scope_guard unmapping( [&](){ UnmapViewOfFile(lpMapView); } );
<强>附录强>:
我还应该提一下标准库智能指针 shared_ptr
和unique_ptr
,它们负责指针所有权,当所有者数量为0时调用删除器。名称意味着他们分别实施共享和独特的所有权。它们都可以将自定义删除器作为参数,但只有shared_ptr
支持在将智能指针复制/移动到基类指针时使用原始指针值调用自定义删除器。
另外,我最好还提一下标准库容器类,特别是vector
,它提供动态大小的可复制数组,具有自动内存管理功能,{{1} },它为string
用于表示字符串的数组的特定情况提供了大致相同的功能。这些课程使您免于直接与char
和new
打交道,并使这些详细信息正确无误。
总而言之,
尽可能使用标准库和/或3 rd 派对容器,
否则使用标准库和/或3 rd 方智能指针,
如果即使这样做并不能满足您的清理需求,也可以定义在析构函数中进行清理的自定义类。
答案 2 :(得分:4)
使用RAII,您可以使用包装器的对象生命周期来规范遗留类型的生命周期,例如您所描述的。 shared_ptr&lt;&gt;加上明确的“自由”函数的模板可以用作这样的包装器。
答案 3 :(得分:2)
据我所知,除非你使用可以做的元素,否则C ++ 11不会关心清理。例如,您可以将此清理代码放入类的析构函数中,并通过创建智能指针来创建它的实例。智能指针在不再使用或共享时会自行删除。如果你创建一个唯一指针并且它被删除,因为它超出了作用域,它会自动调用delete本身,因此你的析构函数被调用,你不需要自己删除/销毁/清理。
请参阅http://www.cplusplus.com/reference/memory/unique_ptr/
这正是C ++ 11新增的自动清理功能。当然,用尽范围的通常类实例也会调用它的析构函数。
答案 4 :(得分:2)
没有!
RAII并不是要把清理放在一边,而是自动完成。清理可以在析构函数调用中完成。模式可能是:
void f() {
ResourceHandler handler(make_resource());
...
}
在范围的末尾破坏ResourceHandler(并进行清理)或抛出异常的地方。
答案 5 :(得分:1)
WIN32 API是一个C API - 你仍然需要自己清理。但是,没有什么能阻止你为WIN32 API编写C ++ RAII包装器。
没有RAII的例子:
void foo
{
HANDLE h = CreateFile(_T("C:\\File.txt"), FILE_READ_DATA, FILE_SHARE_READ,
NULL, OPEN_ALWAYS, 0, NULL);
if ( h != INVALID_HANDLE_VALUE )
{
CloseHandle(h);
}
}
使用RAII:
class smart_handle
{
public:
explicit smart_handle(HANDLE h) : m_H(h) {}
~smart_handle() { if (h != INVALID_HANDLE_VALUE) CloseHandle(m_H); }
private:
HANDLE m_H;
// this is a basic example, could be implemented much more elegantly! (Maybe a template param for "valid" handle values since sometimes 0 or -1 / INVALID_HANDLE_VALUE is used, implement proper copying/moving etc or use std::unique_ptr/std::shared_ptr with a custom deleter as mentioned in the comments below).
};
void foo
{
smart_handle h(CreateFile(_T("C:\\File.txt"), FILE_READ_DATA, FILE_SHARE_READ,
NULL, OPEN_ALWAYS, 0, NULL));
// Destructor of smart_handle class would call CloseHandle if h was not NULL
}
RAII可以在C ++ 98或C ++ 11中使用。
答案 6 :(得分:1)
我真的很喜欢The C++ Programming Language, Fourth Edition
中对RAII的解释具体来说,3.2.1.2
,5.2
和13.3
部分解释了它在一般情况下如何管理泄漏,以及RAII在正确构建具有异常的代码中的作用。< / p>
使用RAII的两个主要原因是:
RAII的工作原理是每个构造函数都应该保护 一个且只有一个 资源。 如果构造函数成功完成,则保证会调用析构函数(即,在由于抛出异常而导致堆栈展开的情况下)。因此,如果要获取3种类型的资源,则每种类型的资源(A,B,C类)应该有一个类,并且第四种聚合类型(D类)可以获取其他3种资源(通过A,B和amp; ; C的构造函数)在D的构造函数初始化列表中。
因此,如果资源1(A类)成功获得,但2(B类)失败并投掷,则不会调用资源3(C类)。因为资源1(A类)的构造函数已经完成,所以它的析构函数被保证被调用。但是,不会调用其他析构函数(B,C或D)。
答案 7 :(得分:-5)
它不会清理`文件*。
如果您打开文件,则必须将其关闭。我想你可能会误读这篇文章。
例如:
class RAII
{
private:
char* SomeResource;
public:
RAII() : SomeResource(new char[1024]) {} //allocated 1024 bytes.
~RAII() {delete[] SomeResource;} //cleaned up allocation.
RAII(const RAII& other) = delete;
RAII(RAII&& other) = delete;
RAII& operator = (RAII &other) = delete;
};
它是RAII类的原因是因为所有资源都在构造函数或分配器函数中分配。当类被销毁时,相同的资源会自动清理,因为析构函数会这样做。
所以创建一个实例:
void NewInstance()
{
RAII instance; //creates an instance of RAII which allocates 1024 bytes on the heap.
} //instance is destroyed as soon as this function exists and thus the allocation is cleaned up
//automatically by the instance destructor.
另见:
void Break_RAII_And_Leak()
{
RAII* instance = new RAII(); //breaks RAII because instance is leaked when this function exits.
}
void Not_RAII_And_Safe()
{
RAII* instance = new RAII(); //fine..
delete instance; //fine..
//however, you've done the deleting and cleaning up yourself / manually.
//that defeats the purpose of RAII.
}
现在举例说明以下课程:
class RAII_WITH_EXCEPTIONS
{
private:
char* SomeResource;
public:
RAII_WITH_EXCEPTIONS() : SomeResource(new char[1024]) {} //allocated 1024 bytes.
void ThrowException() {throw std::runtime_error("Error.");}
~RAII_WITH_EXCEPTIONS() {delete[] SomeResource;} //cleaned up allocation.
RAII_WITH_EXCEPTIONS(const RAII_WITH_EXCEPTIONS& other) = delete;
RAII_WITH_EXCEPTIONS(RAII_WITH_EXCEPTIONS&& other) = delete;
RAII_WITH_EXCEPTIONS& operator = (RAII_WITH_EXCEPTIONS &other) = delete;
};
以及以下功能:
void RAII_Handle_Exception()
{
RAII_WITH_EXCEPTIONS RAII; //create an instance.
RAII.ThrowException(); //throw an exception.
//Event though an exception was thrown above,
//RAII's destructor is still called
//and the allocation is automatically cleaned up.
}
void RAII_Leak()
{
RAII_WITH_EXCEPTIONS* RAII = new RAII_WITH_EXCEPTIONS();
RAII->ThrowException();
//Bad because not only is the destructor not called, it also leaks the RAII instance.
}
void RAII_Leak_Manually()
{
RAII_WITH_EXCEPTIONS* RAII = new RAII_WITH_EXCEPTIONS();
RAII->ThrowException();
delete RAII;
//Bad because you manually created a new instance, it throws and delete is never called.
//If delete was called, it'd have been safe but you've still manually allocated
//and defeated the purpose of RAII.
}
fstream
总是这样做。在堆栈上创建fstream
实例时,它会打开一个文件。当调用函数存在时,fstream
将自动关闭。
对于FILE*
,情况并非如此,因为FILE*
不是类,也没有析构函数。因此,您必须自己关闭FILE*
!
编辑:正如下面的评论所指出的,上面的代码存在根本问题。它缺少复制构造函数,移动构造函数和赋值运算符。
如果没有这些,尝试复制该类将创建其内部资源(指针)的浅表副本。当类被破坏时,它会在指针上调用两次删除!编辑代码以禁止复制和移动。
对于符合RAII概念的课程,它必须遵循三个规则:What is the copy-and-swap idiom?
如果您不想添加复制或移动,您只需使用上面所示的删除或将相应的功能设为私有。