在无法使用RAII的情况下,如何在C ++中“尝试/最终”?

时间:2018-09-11 04:59:04

标签: c++ exception constructor destructor raii

我从沉重的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?像下面这样使用步速构造/破坏并重新掷球是否可以接受?就像我说的那样,我来自C#,我只是使用try/finallyusing模式。

 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;
  }
}

2 个答案:

答案 0 :(得分:8)

使Resource为可移动类型。给它移动构造/分配。然后,您的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);
}

请注意,在此示例中,不需要异常处理逻辑。这一切都可以解决。通过首先创建新资源,如果该创建引发异常,则我们保留先前创建的资源(如果有)。这就提供了强大的异常保证:在发生异常的情况下,对象的状态将完全保留在异常发生之前的状态。

请注意,不需要显式的trycatch块。 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;
        }
    }
};

答案 1 :(得分:2)

在此,我仅将此答案留作参考,以作为一个示例的示例,该示例在OP的完整方案中并未寻求足够的努力。由于OP本身重新抛出了异常,并且显然只是将try / catch子句用于所谓的 RAII 用途,而没有其他用途。

尼科尔·波拉斯(Nicol Bolas)的答案绝对是必经之路。

原始答案:

如果您只想确保在发生任何错误的情况下调用_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:如果希望异常语义如此,以便在初始化失败的情况下不对 resource 进行任何更改,则可以安全地删除对reset()的调用。这是更可取的方式,也就是强异常保证。否则,请将其保留在此处以确保初始化失败时没有资源。

注3:我在propagate_ptr周围使用unique_ptr包装器来保留_resource访问路径下的const成员的const限定,即在使用{ {1}}。不要忘记const ResourceConsumer