C++中finally的实现

19

这是一种在标准C++中实现类似Finally行为的好方法吗?(不使用特殊指针)

class Exception : public Exception
    { public: virtual bool isException() { return true; } };

class NoException : public Exception
    { public: bool isException() { return false; } };


Object *myObject = 0;

try
{
  // OBJECT CREATION AND PROCESSING
  try
  {
    myObject = new Object();

    // Do something with myObject.
  }

  // EXCEPTION HANDLING
  catch (Exception &e)
  {
    // When there is an excepion, handle or throw,
    // else NoException will be thrown.
  }

  throw NoException();
}

// CLEAN UP
catch (Exception &e)
{
  delete myObject;

  if (e.isException()) throw e;
}
  1. 对象未抛出异常 -> 无异常 -> 对象被清理
  2. 对象抛出异常并被处理 -> 无异常 -> 对象被清理
  3. 对象抛出异常并未被处理 -> 抛出异常 -> 对象被清理 -> 异常继续传递

只查看不是从Exception派生的异常,你将无法捕获它们。这是在一种不需要finally的语言中尝试实现"finally"的一种方式。这大致相当于在Java中尝试模拟析构函数。 - Johannes Schaub - litb
6个回答

35

标准答案是使用某种形式的资源获取即初始化(RAII)缩写。基本上,您创建一个变量,其作用域与最后面的代码块相同,然后在对象析构函数中执行finally块中的工作。

try {
   // Some work
}
finally {
   // Cleanup code
}

变成

class Cleanup
{
public:
    ~Cleanup()
    {
        // Cleanup code
    }
}

Cleanup cleanupObj;

// Some work.

这看起来很不方便,但通常有一个预先存在的对象会为您进行清理。在您的情况下,看起来您想在finally块中销毁该对象,这意味着智能指针或唯一指针将实现您所需的功能:

std::unique_ptr<Object> obj(new Object());

或现代的C ++

auto obj = std::make_unique<Object>();
无论抛出哪个异常,对象都将被销毁。回到RAII,这种情况下资源分配是为对象分配内存并构造它,初始化是unique_ptr的初始化。

使用auto_ptr,对象析构函数将总是被调用。在处理异常的过程中,所有在堆栈上构造的对象本身都会被销毁。在这种情况下,这意味着auto_ptr析构函数将被调用,进而销毁该对象。 - David Norman
需要注意的关键点是,对于堆栈分配的C++对象,析构函数总是在对象超出作用域时运行。这意味着它们可以执行任何finally块可以执行的操作。 - Eclipse
是的,Josh的观点是C++的一个关键特性。通常的想法是,当抛出异常时,堆栈会被“展开”。实际上,实现会逐步查找调用堆栈,为堆栈变量调用析构函数并寻找catch子句。 - Steve Jessop
实际上,这个过程没有那么简单,因为编译器可能对代码进行了巧妙的优化,但这是其基本效果。 - Steve Jessop
编译器优化必须被考虑。在观察GCC v9.3时,只有当异常被捕获时(至少在主函数中),才会发生析构。未捕获的异常将导致立即终止程序(根本没有展开)。 - homac

12
不行。构建类似finally的标准方法是将关注点分离(http://en.wikipedia.org/wiki/Separation_of_concerns),并使在try块中使用的对象在其析构函数中自动释放资源(称为“范围绑定资源管理”)。由于析构函数是确定性运行的,不像Java,在此可以依赖它们安全地进行清理。这样,获取资源的对象也将清理资源。
一种特殊的方式是动态内存分配。由于您是获取资源的人,因此必须再次进行清理。在这里,可以使用智能指针。
try {
    // auto_ptr will release the memory safely upon an exception or normal 
    // flow out of the block. Notice we use the "const auto_ptr idiom".
    // http://www.gotw.ca/publications/using_auto_ptr_effectively.htm
    std::auto_ptr<A> const aptr(new A);
} 
// catch...

2
不要寻找“普通指针解决方案”,如果你的意思是想避免RAII。如果你想要一种“好的方式”来做到这一点,在C++中,那就是RAII。因此,要么使用库中的智能指针,要么如果你不能这样做,那就自己编写一个。 - Steve Jessop

5

如果由于某种奇怪的原因您无法访问标准库,则非常容易实现所需的智能指针类型以处理资源。它可能看起来有点冗长,但它比嵌套的try/catch块少得多,并且您只需要定义此模板一次即可管理需要管理的每个资源:

template<typename T>
struct MyDeletable {
    explicit MyDeletable(T *ptr) : ptr_(ptr) { }
    ~MyDeleteable() { delete ptr_; }
private:
    T *ptr_;
    MyDeletable(const MyDeletable &);
    MyDeletable &operator=(const MyDeletable &);
};

void myfunction() {
    // it's generally recommended that these two be done on one line.
    // But it's possible to overdo that, and accidentally write
    // exception-unsafe code if there are multiple parameters involved.
    // So by all means make it a one-liner, but never forget that there are
    // two distinct steps, and the second one must be nothrow.
    Object *myObject = new Object();
    MyDeletable<Object> deleter(myObject);

    // do something with my object

    return;
}

当然,如果您这样做并在其余代码中使用RAII,您最终将需要所有标准和boost智能指针类型的功能。但这是一个开端,并且可以满足您的要求。
在维护编程中,try ... catch方法可能不起作用。CLEAN UP块不能保证被执行:例如,如果“do something”代码提前返回或某种方式抛出非异常内容。另一方面,我的代码中“deleter”的析构函数保证在这两种情况下都会执行(尽管在程序终止时不会执行)。

1
与auto_ptr相比,这是一个糟糕的解决方案(而auto_ptr也不是一个好的解决方案)。通过将删除器与对象分离,很容易陷入一些本不应该被删除的对象意外地与删除器相关联的情况。这会导致更多的错误而不是防止它们。 - jmucchiello

4

我的建议是:不要试图模仿C++中try-finally语句的行为。只需使用RAII即可。这样你会更加幸福。


3

假设你想要删除指针myObject并避免内存泄漏,但是如果在代码中存在一个"return"语句,在你说// Do something with myObject.的位置,你的代码仍然可能无法实现此目标(我假设这里会有真正的代码)。

RAII技术有一个相关的操作,相当于特定对象的析构函数中的"finally"块:

class ResourceNeedingCleanup
{
  private:
    void cleanup(); // action to run at end
  public:
    ResourceNeedingCleanup( /*args here*/) {}
    ~ResourceNeedingCleanup() { cleanup(); }  

    void MethodThatMightThrowException();
};

typedef boost::shared_ptr<ResourceNeedingCleanup> ResourceNeedingCleanupPtr;
// ref-counted smart pointer


class SomeObjectThatMightKeepReferencesToResources
{
   ResourceNeedingCleanupPtr pR;

   void maybeSaveACopy(ResourceNeedingCleanupPtr& p)
   {
      if ( /* some condition is met */ )
         pR = p;
   }
};

// somewhere else in the code:
void MyFunction(SomeObjectThatMightKeepReferencesToResources& O)
{
   ResourceNeedingCleanup R1( /*parameters*/) ;
   shared_ptr<ResourceNeedingCleanup> pR2 = 
        new ResourceNeedingCleanup( /*parameters*/ );
   try
   {
      R1.MethodThatMightThrowException();
      pR2->MethodThatMightThrowException();
      O->maybeSaveACopy(pR2);
   }
   catch ( /* something */ )
   {
      /* something */
   }

   // when we exit this block, R1 goes out of scope and executes its destructor
   // which calls cleanup() whether or not an exception is thrown.
   // pR2 goes out of scope. This is a shared reference-counted pointer. 
   // If O does not save a copy of pR2, then pR2 will be deleted automatically
   // at this point. Otherwise, pR2 will be deleted automatically whenever
   // O's destructor is called or O releases its ownership of pR2 and the
   // reference count goes to zero.
}

我认为语义是正确的;我自己并没有经常使用shared_ptr,但我更喜欢它而不是auto_ptr<>--一个指向对象的指针只能被一个auto_ptr<>所“拥有”。我使用过COM的CComPtr和我自己编写的一种变体,用于“普通”(非COM)对象,类似于shared_ptr<>但具有Attach()和Detach()以便将指针从一个智能指针转移到另一个。

好的,现在我明白了... 如果你在列出代码之前先解释一下你通常在寻找什么,那会更有帮助。 - Jason S

2
直接回答你的问题,不行
这是一种聪明的实现功能的方式,但它是不可靠的。其中一个失败的方式是,如果你的“做某事”代码抛出的异常不是从Exception派生的,那么你将永远无法delete myObject
这里有一个更重要的问题,那就是程序员采用的任何特定语言的方法论。你听到RAII的原因是,比你或我经验丰富得多的程序员发现,在C++编程领域,该方法是可靠的。你可以依赖其他程序员使用它,而其他程序员也会希望依赖你使用它。

2
"比你或我有更多经验的程序员" - 这是一个很好的说法,尽管请注意他们也通过理性的力量说服了我们,这不仅仅是一种权威诉求 ;-)" - Steve Jessop

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接