C ++是否支持"最终"块? (这是我一直在听到的" RAII"是什么?)

时间:2020-03-06 15:00:47  来源:igfitidea点击:

C ++是否支持"最终"块?

什么是RAII习语?

C ++的RAII习惯用法和C#的" using"语句之间有什么区别?

解决方案

不,C ++不支持" finally"块。原因是C ++而是支持RAII:"资源获取是否初始化"-一个不好的名字?一个非常有用的概念。

这个想法是对象的析构函数负责释放资源。当对象具有自动存储持续时间时,对象的析构函数将在创建对象的块退出时被调用-即使该块在存在异常的情况下退出。这是Bjarne Stroustrup对这个主题的解释。

RAII的常见用法是锁定互斥锁:

// A class with implements RAII
class lock
{
    mutex &m_;

public:
    lock(mutex &m)
      : m_(m)
    {
        m.acquire();
    }
    ~lock()
    {
        m_.release();
    }
};

// A class which uses 'mutex' and 'lock' objects
class foo
{
    mutex mutex_; // mutex for locking 'foo' object
public:
    void bar()
    {
        lock scopeLock(mutex_); // lock object.

        foobar(); // an operation which may throw an exception

        // scopeLock will be destructed even if an exception
        // occurs, which will release the mutex and allow
        // other functions to lock the object and run.
    }
};

RAII还简化了将对象用作其他类的成员的操作。当拥有类被销毁时,由RAII类管理的资源将被释放,因为结果是将调用由RAII管理的类的析构函数。这意味着当我们对所有管理资源的类中的所有成员使用RAII时,我们可以为所有者类使用非常简单的,甚至是默认的析构函数,因为它不需要手动管理其成员资源寿命。 (感谢Mike B指出这一点。)

对于熟悉Cor VB.NET的人,我们可能会认识到RAII与使用IDisposable和'using'语句的.NET确定性销毁类似。确实,这两种方法非常相似。主要区别在于RAII将确定性地释放任何类型的资源-包括内存。在.NET(甚至.NET语言C ++ / CLI)中实现IDisposable时,将确定性地释放除内存以外的资源。在.NET中,内存不是确定释放的;内存仅在垃圾回收周期中释放。

?有人认为"销毁是资源的放弃"是RAII惯用语的更准确的称呼。

FWIW,Microsoft Visual C ++最终确实支持try,并且它在MFC应用程序中一直被用作捕获严重异常的一种方法,否则可能导致崩溃。例如;

int CMyApp::Run() 
{
    __try
    {
        int i = CWinApp::Run();
        m_Exitok = MAGIC_EXIT_NO;
        return i;
    }
    __finally
    {
        if (m_Exitok != MAGIC_EXIT_NO)
            FaultHandler();
    }
}

我过去曾用它来做类似退出前保存打开文件的备份之类的事情。但是,某些JIT调试设置将破坏此机制。

除了使基于堆栈的对象易于清理之外,RAII也是有用的,因为当对象是另一个类的成员时,会发生相同的"自动"清理。当拥有的类被破坏时,由RAII类管理的资源将被清理,因为结果是该类的dtor被调用。

这意味着当我们到达RAII必杀技并且类中的所有成员都使用RAII(如智能指针)时,我们可以为所有者类使用一个非常简单(甚至甚至是默认)的dtor,因为它不需要手动管理成员资源生存期。

在C ++中,由于RAII,最后不需要。

RAII将异常安全的责任从对象的用户转移到对象的设计者(和实现者)。我认为这是正确的地方,因为我们只需要一次正确的异常安全性(在设计/实现中)。通过最终使用,我们每次使用对象时都需要获得正确的异常安全性。

此外,IMO代码看起来更整洁(请参阅下文)。

例子:

数据库对象。为了确保使用数据库连接,必须将其打开和关闭。通过使用RAII,可以在构造函数/析构函数中完成此操作。

像RAII一样的C ++

void someFunc()
{
    DB    db("DBDesciptionString");
    // Use the db object.

} // db goes out of scope and destructor closes the connection.
  // This happens even in the presence of exceptions.

RAII的使用使正确使用数据库对象变得非常容易。无论我们如何尝试和滥用它,数据库对象都将通过使用析构函数正确关闭自身。

Java终于如愿以偿

void someFunc()
{
    DB      db = new DB("DBDesciptionString");
    try
    {
        // Use the db object.
    }
    finally
    {
        // Can not rely on finaliser.
        // So we must explicitly close the connection.
        try
        {
            db.close();
        }
        catch(Throwable e)
        {
           /* Ignore */
           // Make sure not to throw exception if one is already propagating.
        }
    }
}

最终使用时,将对象的正确使用委托给对象的用户。即,对象用户有责任正确地显式关闭数据库连接。现在我们可能会争辩说可以在终结器中完成此操作,但是资源可能具有有限的可用性或者其他约束,因此我们通常确实希望控制对象的释放,而不是依赖于垃圾收集器的不确定性行为。

这也是一个简单的例子。
当我们需要释放多个资源时,代码可能会变得很复杂。

可以在这里找到更详细的分析:http://accu.org/index.php/journals/236

try
{
  ...
  goto finally;
}
catch(...)
{
  ...
  goto finally;
}
finally:
{
  ...
}

很抱歉,我们会发现这样一个旧线程,但是在以下推理中存在一个重大错误:

RAII moves the responsibility of exception safety from the user of the object to the designer (and implementer) of the object. I would argue this is the correct place as you then only need to get exception safety correct once (in the design/implementation). By using finally you need to get exception safety correct every time you use an object.

通常,我们必须处理动态分配的对象,对象的动态数量等。在try块中,某些代码可能会创建许多对象(在运行时确定多少个对象)并将其指针存储在列表中。现在,这不是一个奇特的情况,而是非常普遍的情况。在这种情况下,我们想要编写类似

void DoStuff(vector<string> input)
{
  list<Foo*> myList;

  try
  {    
    for (int i = 0; i < input.size(); ++i)
    {
      Foo* tmp = new Foo(input[i]);
      if (!tmp)
        throw;

      myList.push_back(tmp);
    }

    DoSomeStuff(myList);
  }
  finally
  {
    while (!myList.empty())
    {
      delete myList.back();
      myList.pop_back();
    }
  }
}

当然,超出范围时,列表本身将被销毁,但这不会清除我们创建的临时对象。

相反,我们必须走丑陋的路线:

void DoStuff(vector<string> input)
{
  list<Foo*> myList;

  try
  {    
    for (int i = 0; i < input.size(); ++i)
    {
      Foo* tmp = new Foo(input[i]);
      if (!tmp)
        throw;

      myList.push_back(tmp);
    }

    DoSomeStuff(myList);
  }
  catch(...)
  {
  }

  while (!myList.empty())
  {
    delete myList.back();
    myList.pop_back();
  }
}

还有:为什么无论如何垃圾资源收集器都会自动释放资源,即使托管的容器也能提供finally-block?

提示:使用" finally"可以做的不仅仅是内存释放。

why is it that even managed languages provide a finally-block despite resources being deallocated automatically by the garbage collector anyway?

实际上,基于垃圾收集器的语言还需要"更多"。垃圾收集器无法及时销毁对象,因此不能依靠它来正确清理与内存无关的问题。

在动态分配数据方面,许多人认为我们应该使用智能指针。

然而...

RAII moves the responsibility of exception safety from the user of the object to the designer

可悲的是,这是它自己的失败。旧的C编程习惯死得很重。当我们使用以C或者非常C风格编写的库时,将不会使用RAII。无需重新编写整个API前端,这就是我们必须使用的方法。然后,缺乏"最终"的确令人难过。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。