当前位置:  开发笔记 > 编程语言 > 正文

C++是否支持'finally'块?(我听到的'RAII'是什么?)

如何解决《C++是否支持'finally'块?(我听到的'RAII'是什么?)》经验,为你挑选了9个好方法。

C++是否支持' finally '块?

什么是RAII成语

C++的RAII习语与C#的'using'语句有什么区别?



1> Kevin..:

不,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指出这一点.)

对于那些使用C#或VB.NET的人来说,你可能会认识到RAII类似于使用IDisposable和'using'语句的.NET确定性破坏.的确,这两种方法非常相似.主要区别在于RAII将确定性地释放任何类型的资源 - 包括内存.在.NET中实现IDisposable(甚至是.NET语言C++/CLI)时,除了内存之外,资源将被确定性地释放.在.NET中,内存不是确定性释放的; 内存仅在垃圾回收周期中释放.

 

†有些人认为"毁灭是资源放弃"是RAII习语的更准确的名称.


SBRM ==范围界限资源管理
当你有一些东西需要清理时,这会让你陷入困境,这与任何C++对象的生命周期都不相符.我猜你最终得到了Lifetime Equals C++ Class Liftime或者它变得丑陋(LECCLEOEIGU?).
"毁灭是资源放弃" - DIRR ......不,对我不起作用.= P
RAII陷入困境 - 真的没有改变它.试图这样做是愚蠢的.但是,您必须承认,"资源获取是初始化"仍然是一个非常差的名称.
任何一方都不仅能够设计软件,更不用说改进技术的人,也无法为这样一个可怕的缩写提供任何有价值的借口.
@WarrenP不,这被称为单一责任原则.
@Jasper:这就是为什么在每个代码库中都有这么多几乎完全没用的类.因为C++没有别的选择.这被称为语言疣.
我总是喜欢一个类的唯一责任,因为它里面有更多的带宽,而不是"用这种语言的局限性".
@Kevin:在使用过程中,RAII实际上与`using`不相似:你不能在你的类'初始化器/终结器上使用`using`语句.你必须要明确,不要忘记`using`语句.嵌套变得可怕."使用"与RAII的用例基本上是徒劳的:作用域锁,向量,字符串,共享指针,事务以及所有那些无需手动编写`using()`的用例.在你呈现它的方式中,"使用"在RAII之前是一个糟糕的防御,并且我意识到许多.net-devs努力说服自己RAII是相似的,但在他们的心里他们(应该)知道它不是.
@WarrenP然后你应该将"某些东西"包装成一个类.这是RAII的整个核心,事实上它往往会迫使你进入更好的设计.

2> Martin York..:

在C++中总算是不是必需的,因为RAII的.

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

此外,IMO代码看起来更整洁(见下文).

例:

数据库对象.要确保使用DB连接,必须打开和关闭它.通过使用RAII,可以在构造函数/析构函数中完成.

C++与RAII一样

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使得正确使用DB对象非常容易.无论我们如何尝试并滥用它,DB对象都将通过使用析构函数正确关闭自身.

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


`//如果一个人已经在传播,请确保不要抛出异常.对于C++析构函数,由于这个原因,不要抛出异常也很重要.
@Cemafor:C++不会从析构函数中抛出异常的原因与Java不同.在Java中它可以工作(你只是松开原来的例外).在C++中它真的很糟糕.但是C++中的要点是,当你编写析构函数时,你只需要做一次(由类的设计者).在Java中,您必须在使用时执行此操作.因此,班级用户有责任及时编写相同的锅炉板.

3> Paolo.Bolzon..:

在C++ 11中,如果需要,RAII允许最终:

namespace detail { //adapt to your "private" namespace
template 
struct FinalAction {
    FinalAction(F f) : clean_{f} {}
   ~FinalAction() { if(enabled_) clean_(); }
    void disable() { enabled_ = false; };
  private:
    F clean_;
    bool enabled_{true}; }; }

template 
detail::FinalAction finally(F f) {
    return detail::FinalAction(f); }

使用示例:

#include 
int main() {
    int* a = new int;
    auto delete_a = finally([a] { delete a; std::cout << "leaving the block, deleting a!\n"; });
    std::cout << "doing something ...\n"; }

输出将是:

doing something...
leaving the block, deleting a!

我个人用了几次来确保在C++程序中关闭POSIX文件描述符.

拥有一个管理资源的真正的类,因此避免任何类型的泄漏通常会更好,但这最终在使类听起来像过度杀伤的情况下非常有用.

此外,我最终比其他语言更喜欢它,因为如果自然使用你在开始代码附近编写结束代码(在我的例子中是newdelete),并且破坏遵循LIFO顺序构造,就像在C++中一样.唯一的缺点是你得到一个你并没有真正使用的自动变量,lambda语法会让它有点吵(在我的例子中,第四行只有最后一个单词,而右边的{} -block是有意义的,休息基本上是噪音).

另一个例子:

 [...]
 auto precision = std::cout.precision();
 auto set_precision_back = finally( [precision, &std::cout]() { std::cout << std::setprecision(precision); } );
 std::cout << std::setprecision(3);

禁用该成员是否有用最终只有在失败的情况下被调用.例如,您必须在三个不同的容器中复制一个对象,您可以设置finally以撤消每个副本,并在所有副本成功后禁用.这样做,如果破坏不能扔,你保证强有力的保证.

禁用示例:

//strong guarantee
void copy_to_all(BIGobj const& a) {
    first_.push_back(a);
    auto undo_first_push = finally([first_&] { first_.pop_back(); });

    second_.push_back(a);
    auto undo_second_push = finally([second_&] { second_.pop_back(); });

    third_.push_back(a);
    //no necessary, put just to make easier to add containers in the future
    auto undo_third_push = finally([third_&] { third_.pop_back(); });

    undo_first_push.disable();
    undo_second_push.disable();
    undo_third_push.disable(); }


@ Paolo.Bolzoni很抱歉没有尽快回复,我没有收到您的评论通知.我担心finally块(我称之为DLL函数)将在作用域结束之前调用(因为该变量未使用),但后来在SO上发现了一个问题,这清除了我的后顾之忧.我会链接到它,但不幸的是,我再也找不到它了.

4> Michael Burr..:

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

这意味着当你到达RAII天堂并且一个类中的所有成员都使用RAII(如智能指针)时,你可以为所有者类放弃一个非常简单(甚至可能是默认的)dtor,因为它不需要手动管理它成员资源生命周期.



5> Philip Couli..:

为什么即使托管语言提供了最终阻止,尽管垃圾收集器自动解除分配资源?

实际上,基于垃圾收集器的语言需要"最终"更多.垃圾收集器不会及时销毁您的对象,因此无法依赖它来正确清除与内存无关的问题.

就动态分配的数据而言,许多人会争辩说你应该使用智能指针.

然而...

RAII将异常安全的责任从对象的用户转移到设计者

可悲的是,这是它自己的垮台.旧的C编程习惯很难.当您使用以C或非常C风格编写的库时,将不会使用RAII.没有重写整个API前端,这正是你必须要处理的. 然后缺乏"终于"真的咬人.


确切地说......从理想的角度来看,RAII看起来很不错.但是我必须一直使用传统的C API(比如Win32 API中的C风格函数......).获取返回某种HANDLE的资源是很常见的,然后需要像CloseHandle(HANDLE)这样的函数进行清理.使用try ... finally是处理可能异常的好方法.(值得庆幸的是,看起来shared_ptr与自定义删除器和C++ 11 lambdas应该提供一些基于RAII的缓解,不需要编写整个类来包装一些我只在一个地方使用的API.).
@JamesJohnston,编写一个包含任何类型句柄并提供RAII机制的包装类非常容易.例如,ATL提供了一堆.看起来你认为这太麻烦但是我不同意,它们非常小而且易于编写.
简单的是,小号没有.大小取决于您正在使用的库的复杂程度.
@couling:在很多情况下程序会调用`SomeObject.DoSomething()`方法并想知道它是否成功,(2)失败*没有副作用*,(3)失败了 - 影响呼叫者准备应对,或(4)失败的副作用,呼叫者无法应付.只有来电者才会知道它能够和无法应对的情况; 呼叫者需要的是一种了解情况的方法.太糟糕了,没有标准机制来提供有关异常的最重要信息.

6> anton_rh..:

使用C ++ 11 lambda函数的另一个“最终”块仿真

template 
inline void with_finally(const TCode &code, const TFinallyCode &finally_code)
{
    try
    {
        code();
    }
    catch (...)
    {
        try
        {
            finally_code();
        }
        catch (...) // Maybe stupid check that finally_code mustn't throw.
        {
            std::terminate();
        }
        throw;
    }
    finally_code();
}

希望编译器可以优化上面的代码。

现在我们可以编写如下代码:

with_finally(
    [&]()
    {
        try
        {
            // Doing some stuff that may throw an exception
        }
        catch (const exception1 &)
        {
            // Handling first class of exceptions
        }
        catch (const exception2 &)
        {
            // Handling another class of exceptions
        }
        // Some classes of exceptions can be still unhandled
    },
    [&]() // finally
    {
        // This code will be executed in all three cases:
        //   1) exception was not thrown at all
        //   2) exception was handled by one of the "catch" blocks above
        //   3) exception was not handled by any of the "catch" block above
    }
);

如果愿意,可以将此成语包装到“ try-finally”宏中:

// Please never throw exception below. It is needed to avoid a compilation error
// in the case when we use "begin_try ... finally" without any "catch" block.
class never_thrown_exception {};

#define begin_try    with_finally([&](){ try
#define finally      catch(never_thrown_exception){throw;} },[&]()
#define end_try      ) // sorry for "pascalish" style :(

现在,“最终”块在C ++ 11中可用:

begin_try
{
    // A code that may throw
}
catch (const some_exception &)
{
    // Handling some exceptions
}
finally
{
    // A code that is always executed
}
end_try; // Sorry again for this ugly thing

我个人不喜欢“ finally”惯用语的“ macro”版本,并且宁愿使用纯的“ with_finally”功能,即使在这种情况下语法更庞大。

您可以在此处测试上面的代码:http : //coliru.stacked-crooked.com/a/1d88f64cb27b3813

聚苯乙烯

如果您的代码中需要一个finally块,那么作用域保护或ON_FINALLY / ON_EXCEPTION宏可能会更适合您的需求。

这是用法ON_FINALLY / ON_EXCEPTION的简短示例:

void function(std::vector &vector)
{
    int *arr1 = (int*)malloc(800*sizeof(int));
    if (!arr1) { throw "cannot malloc arr1"; }
    ON_FINALLY({ free(arr1); });

    int *arr2 = (int*)malloc(900*sizeof(int));
    if (!arr2) { throw "cannot malloc arr2"; }
    ON_FINALLY({ free(arr2); });

    vector.push_back("good");
    ON_EXCEPTION({ vector.pop_back(); });

    ...



7> 小智..:

很抱歉挖掘这样一个旧线程,但在以下推理中存在重大错误:

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

通常情况下,您必须处理动态分配的对象,动态数量的对象等.在try-block中,某些代码可能会创建许多对象(在运行时确定多少个对象)并在列表中存储指向它们的指针.现在,这不是一个奇特的场景,但很常见.在这种情况下,你想写一些像

void DoStuff(vector input)
{
  list 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 input)
{
  list 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();
  }
}

另外:为什么即使管理语言提供了最终阻止,尽管垃圾收集器自动解除分配资源?

提示:除了内存释放之外,你还能做更多的事情.


托管语言需要最终块,因为只有一种资源是自动管理的:内存.RAII意味着所有资源都可以以相同的方式处理,因此不需要最终.如果您在示例中实际使用了RAII(通过在列表中使用智能指针而不是裸指针),则代码将比"finally"示例更简单.如果你不检查新的返回值,甚至更简单 - 检查它是非常毫无意义的.
你的例子看起来如此可怕的原因并不是因为RAII存在缺陷,而是因为你没有使用它.原始指针不是RAII.
`new`不返回NULL,而是抛出异常
你提出了一个重要的问题,但确实有两个可能的答案.一个是Myto给出的 - 使用智能指针进行所有动态分配.另一种是使用标准容器,它们在销毁时总会破坏其内容物.无论哪种方式,每个分配的对象最终都由静态分配的对象拥有,该对象在销毁时自动释放它.由于普通指针和数组的高可见性,这些更好的解决方案很难让程序员发现,这真是一种耻辱.
C++ 11对此进行了改进,并在stdlib中直接包含`std :: shared_ptr`和`std :: unique_ptr`.

8> SmacL..:

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调试设置会打破这种机制.


请记住,这不是C++异常,而是SEH异常.您可以在MS C++代码中使用它们.SEH是一个OS异常处理程序,它是VB,.NET实现异常的方式.
SEH很糟糕,也阻止了C++析构函数的调用

9> tobi_s..:

正如其他答案所指出的,C++可以支持finally类似功能.这个功能的实现可能最接近于标准语言的一部分,是C++核心指南的附件,这是一套使用Bjarne Stoustrup和Herb Sutter编辑的C++的最佳实践.的实施finally是部分指引支持库(GSL).在整个指南中,finally建议在处理旧式接口时使用它,并且它还有自己的指南,如果没有合适的资源句柄,则使用标题为使用final_action对象来表示清理.

因此,不仅C++支持finally,实际上建议在很多常见用例中使用它.

GSL实现的示例用法如下:

#include 

void example()
{
    int handle = get_some_resource();
    auto handle_clean = gsl::finally([&handle] { clean_that_resource(handle); });

    // Do a lot of stuff, return early and throw exceptions.
    // clean_that_resource will always get called.
}

GSL的实现和使用与Paolo.Bolzoni的答案非常相似.一个区别是由于gsl::finally()缺少disable()调用而创建的对象.如果您需要该功能(比如,一旦组装完就返回资源并且不会发生任何异常),您可能更喜欢Paolo的实现.否则,使用GSL就像使用标准化功能一样接近.

推荐阅读
吻过彩虹的脸_378
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有