当无法使用 RAII 时,如何在 C++ 中执行 "try/finally"?
How to do "try/finally" in C++ when RAII is not possible?
我从沉重的 C# 背景回到 C++,我继承了一些 C++ 代码库,我认为这些代码库可能不符合最佳 C++ 实践。
例如,我正在处理以下情况(简化):
// resource
class Resource {
HANDLE _resource = NULL;
// copying not allowed
Resource(const Resource&);
Resource& operator=(const Resource& other);
public:
Resource(std::string name) {
_resource = ::GetResource(name); if (NULL == _resource) throw "Error"; }
~Resource() {
if (_resource != NULL) { CloseHandle(_resource); _resource = NULL; };
}
operator HANDLE() const { return _resource; }
};
// resource consumer
class ResourceConsumer {
Resource _resource;
// ...
public:
void Initialize(std::string name) {
// initialize the resource
// ...
// do other things which may throw
}
}
这里 ResourceConsumer
创建了一个 Resource
的实例并做了一些其他的事情。出于某种原因(在我的控制之外),它为此公开了 Initialize
方法,而不是提供非默认构造函数,这显然违反了 RAII 模式。这是一个库代码,API 不能在不进行重大更改的情况下进行重构。
所以我的问题是,在这种情况下如何正确编码 Initialize
?使用 in-pace construction/destruction 和 re-throw 是否可以接受,如下所示?正如我所说,我来自 C#,在那里我只使用 try/finally
或 using
模式。
void ResourceConsumer::Initialize(std::string name) {
// first destroy _resource in-place
_resource.~Resource();
// then construct it in-place
new (&_resource) Resource(name);
try {
// do other things which may throw
// ...
}
catch {
// we don't want to leave _resource initialized if anything goes wrong
_resource.~Resource();
throw;
}
}
我把这个答案留在这里仅供参考,作为一个没有足够努力地寻找 OP 的完整场景的答案的例子。由于 OP 本身 重新抛出 例外,并且显然只是将 try/catch 子句用于所谓的 RAII 目的,没有其他用途。
尼可波拉斯的回答绝对是正确的选择。
原回答:
如果你只想确保 _resource
的析构函数被调用,以防万一 出现任何问题 ,那么你可以 Resource _resource
一些独特的智能指针,然后在 ResourceConsumer::Initialize()
范围内创建一个临时智能指针,如果一切顺利,最终将临时移动到 _resource
。在所有其他情况下,范围将在移动之前退出并且堆栈展开将为临时调用适当的析构函数。
代码示例,试图尽可能地坚持问题中的代码段:
// resource consumer
class ResourceConsumer {
template<class T> using prop_ptr = std::experimental::propagate_const<std::unique_ptr<T>>;
prop_ptr<Resource> _resource;
// ...
public:
void Initialize(std::string name);
};
void ResourceConsumer::Initialize(std::string name) {
// first destroy _resource in-place
std::experimental::get_underlying(_resource).reset(); // See 'Note 2' below.
// then construct it in-place
auto tempPtr = std::make_unique<Resource>(name);
// do other things which may throw
// ...
// Initialization is done successfully, move the newly created one onto your member
_resource = move(tempPtr);
// we don't want to leave _resource initialized if anything goes wrong
// Fortunately, in case we didn't get here, tempPtr is already being destroyed after the next line, and _resource remains empty :-)
}
注意 1:由于我意识到 catch
子句只是重新抛出,所以我们在没有它的情况下也能获得相同的效果。
注意 2:您可以安全地删除对 reset()
的调用,以防您希望异常语义在初始化失败的情况下不对 resource。这是更好的方式,a.k.a。 强异常保证。否则将其留在那里以保证初始化失败时资源为空。
注意 3:我在 unique_ptr
周围使用 propagate_ptr
包装器来保留 const
访问路径下 _resource
成员的常量限定,即在使用一个const ResourceConsumer
。别忘了 #include <experimental/propagate_const>
.
将Resource
设为活字印刷。给它移动construction/assignment。然后,您的 Initialize
方法可以如下所示:
void ResourceConsumer::Initialize(std::string name)
{
//Create the resource *first*.
Resource res(name);
//Move the newly-created resource into the current one.
_resource = std::move(res);
}
请注意,在此示例中,不需要异常处理逻辑。这一切都会自行解决。通过首先创建新资源,如果该创建引发异常,那么我们保留 先前创建的资源(如果有)。这提供了强大的异常保证:在发生异常时,对象的状态将与异常发生前的状态保持一致。
请注意,不需要明确的 try
和 catch
块。 RAII 正常工作。
您的 Resource
移动操作将是这样的:
class Resource {
public:
Resource() = default;
Resource(std::string name) : _resource(::GetResource(name))
{
if(_resource == NULL) throw "Error";
}
Resource(Resource &&res) noexcept : _resource(res._resource)
{
res._resource = NULL;
}
Resource &operator=(Resource &&res) noexcept
{
if(&res != this)
{
reset();
_resource = res._resource;
res._resource = NULL;
}
}
~Resource()
{
reset();
}
operator HANDLE() const { return _resource; }
private:
HANDLE _resource = NULL;
void reset() noexcept
{
if (_resource != NULL)
{
CloseHandle(_resource);
_resource = NULL;
}
}
};
我从沉重的 C# 背景回到 C++,我继承了一些 C++ 代码库,我认为这些代码库可能不符合最佳 C++ 实践。
例如,我正在处理以下情况(简化):
// resource
class Resource {
HANDLE _resource = NULL;
// copying not allowed
Resource(const Resource&);
Resource& operator=(const Resource& other);
public:
Resource(std::string name) {
_resource = ::GetResource(name); if (NULL == _resource) throw "Error"; }
~Resource() {
if (_resource != NULL) { CloseHandle(_resource); _resource = NULL; };
}
operator HANDLE() const { return _resource; }
};
// resource consumer
class ResourceConsumer {
Resource _resource;
// ...
public:
void Initialize(std::string name) {
// initialize the resource
// ...
// do other things which may throw
}
}
这里 ResourceConsumer
创建了一个 Resource
的实例并做了一些其他的事情。出于某种原因(在我的控制之外),它为此公开了 Initialize
方法,而不是提供非默认构造函数,这显然违反了 RAII 模式。这是一个库代码,API 不能在不进行重大更改的情况下进行重构。
所以我的问题是,在这种情况下如何正确编码 Initialize
?使用 in-pace construction/destruction 和 re-throw 是否可以接受,如下所示?正如我所说,我来自 C#,在那里我只使用 try/finally
或 using
模式。
void ResourceConsumer::Initialize(std::string name) {
// first destroy _resource in-place
_resource.~Resource();
// then construct it in-place
new (&_resource) Resource(name);
try {
// do other things which may throw
// ...
}
catch {
// we don't want to leave _resource initialized if anything goes wrong
_resource.~Resource();
throw;
}
}
我把这个答案留在这里仅供参考,作为一个没有足够努力地寻找 OP 的完整场景的答案的例子。由于 OP 本身 重新抛出 例外,并且显然只是将 try/catch 子句用于所谓的 RAII 目的,没有其他用途。
尼可波拉斯的回答绝对是正确的选择。
原回答:
如果你只想确保 _resource
的析构函数被调用,以防万一 出现任何问题 ,那么你可以 Resource _resource
一些独特的智能指针,然后在 ResourceConsumer::Initialize()
范围内创建一个临时智能指针,如果一切顺利,最终将临时移动到 _resource
。在所有其他情况下,范围将在移动之前退出并且堆栈展开将为临时调用适当的析构函数。
代码示例,试图尽可能地坚持问题中的代码段:
// resource consumer
class ResourceConsumer {
template<class T> using prop_ptr = std::experimental::propagate_const<std::unique_ptr<T>>;
prop_ptr<Resource> _resource;
// ...
public:
void Initialize(std::string name);
};
void ResourceConsumer::Initialize(std::string name) {
// first destroy _resource in-place
std::experimental::get_underlying(_resource).reset(); // See 'Note 2' below.
// then construct it in-place
auto tempPtr = std::make_unique<Resource>(name);
// do other things which may throw
// ...
// Initialization is done successfully, move the newly created one onto your member
_resource = move(tempPtr);
// we don't want to leave _resource initialized if anything goes wrong
// Fortunately, in case we didn't get here, tempPtr is already being destroyed after the next line, and _resource remains empty :-)
}
注意 1:由于我意识到 catch
子句只是重新抛出,所以我们在没有它的情况下也能获得相同的效果。
注意 2:您可以安全地删除对 reset()
的调用,以防您希望异常语义在初始化失败的情况下不对 resource。这是更好的方式,a.k.a。 强异常保证。否则将其留在那里以保证初始化失败时资源为空。
注意 3:我在 unique_ptr
周围使用 propagate_ptr
包装器来保留 const
访问路径下 _resource
成员的常量限定,即在使用一个const ResourceConsumer
。别忘了 #include <experimental/propagate_const>
.
将Resource
设为活字印刷。给它移动construction/assignment。然后,您的 Initialize
方法可以如下所示:
void ResourceConsumer::Initialize(std::string name)
{
//Create the resource *first*.
Resource res(name);
//Move the newly-created resource into the current one.
_resource = std::move(res);
}
请注意,在此示例中,不需要异常处理逻辑。这一切都会自行解决。通过首先创建新资源,如果该创建引发异常,那么我们保留 先前创建的资源(如果有)。这提供了强大的异常保证:在发生异常时,对象的状态将与异常发生前的状态保持一致。
请注意,不需要明确的 try
和 catch
块。 RAII 正常工作。
您的 Resource
移动操作将是这样的:
class Resource {
public:
Resource() = default;
Resource(std::string name) : _resource(::GetResource(name))
{
if(_resource == NULL) throw "Error";
}
Resource(Resource &&res) noexcept : _resource(res._resource)
{
res._resource = NULL;
}
Resource &operator=(Resource &&res) noexcept
{
if(&res != this)
{
reset();
_resource = res._resource;
res._resource = NULL;
}
}
~Resource()
{
reset();
}
operator HANDLE() const { return _resource; }
private:
HANDLE _resource = NULL;
void reset() noexcept
{
if (_resource != NULL)
{
CloseHandle(_resource);
_resource = NULL;
}
}
};