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


Answers:


273

不,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(甚至是.NET语言C ++ / CLI)中实现IDisposable时,将确定性地释放除内存以外的资源。在.NET中,内存不是确定释放的;内存仅在垃圾回收周期中释放。

 

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


18
“破坏就是放弃资源”-DIRR ...不,对我不起作用。= P
Erik Forbes

14
RAII卡住了-确实没有改变。这样做是愚蠢的。但是,您必须承认“ Resource Acquisition Is Initialization”仍然很差劲。
凯文”

162
SBRM ==范围绑定资源管理
Johannes Schaub-litb

10
任何不仅具有软件工程能力的人,更不用说改进的技术,都不能为这种可怕的缩写提供任何借口。
Hardryv'4

54
当您要清理的东西与任何C ++对象的生存期都不匹配时,这会使您陷入困境。我猜您最终会遇到Lifetime Equals C ++类Liftime或其他情况变得很丑陋(LECCLEOEIGU?)。
沃伦·P

79

在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.
        }
    }
}

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

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

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


16
// Make sure not to throw exception if one is already propagating.出于这个原因,对于C ++析构函数也不要抛出异常非常重要。
Cemafor

10
@Cemafor:C ++不将异常抛出析构函数的原因与Java不同。在Java中,它将起作用(您只需松开原始异常)。在C ++中,它的确很糟糕。但是C ++的要点是,当他编写析构函数时,您只需(由类的设计者)执行一次。在Java中,必须在使用时进行操作。因此,班级用户有责任及时编写相同的样板。
马丁·约克

1
如果是“需要”的问题,那么您也不需要RAII。让我们摆脱它!:-)开个玩笑,RAII在很多情况下都可以。当您想执行一些代码(与资源无关)时,即使上面的代码提早返回,RAII的工作也变得更加麻烦。为此,您可以使用gotos或将其分为两种方法。
2014年

1
@Trinidad:这不是您想的那么简单(因为您的所有建议似乎都选择了最糟糕的选择)。这就是为什么问题比注释更适合探讨这个问题的原因。
马丁·约克

1
批评“由于RAII并不需要”:在很多情况下,添加临时RAII会增加太多样板代码,而最后尝试仅是非常合适的。
ceztko

63

RAII通常更好,但是您可以轻松地在C ++中拥有final语义。使用少量的代码。

此外,C ++ 核心指南最终给出了。

这是GSL Microsoft实施的链接和Martin Moene实施的链接

Bjarne Stroustrup多次表示,GSL中的所有内容最终都意味着要纳入标准。因此,最终使用应该是一种面向未来的方法。

如果需要,您可以轻松实现自己,继续阅读。

在C ++ 11中,RAII和lambdas最终可以作一个概括:

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

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

使用示例:

#include <iostream>
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),并且破坏会按照C ++中的LIFO顺序进行。唯一的缺点是,您会得到一个您并没有真正使用的自动变量,而lambda语法使它有点嘈杂(在我的示例第四行中,仅单词 finally和右侧的{} -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(); }

如果您不能使用C ++ 11,那么您仍然可以使用finally,但是代码变得冗长。只需定义一个仅包含构造函数和析构函数的结构,构造函数将引用所需的任何内容,然后析构函数执行所需的操作。基本上,这就是lambda所做的,是手动完成的。

#include <iostream>
int main() {
    int* a = new int;

    struct Delete_a_t {
        Delete_a_t(int* p) : p_(p) {}
       ~Delete_a_t() { delete p_; std::cout << "leaving the block, deleting a!\n"; }
        int* p_;
    } delete_a(a);

    std::cout << "doing something ...\n"; }

可能存在问题:在函数“ finally(F f)”中,它返回一个FinalAction对象,因此可能在最终返回函数之前调用解构函数。也许我们应该使用std ::函数代替模板F.
user1633272

注意,FinalAction这与流行的ScopeGuard用法基本相同,只是名称不同。
安德拉斯(Anderas)'17

1
此优化安全吗?
Nulano

2
@ Paolo.Bolzoni对不起,您没有尽快回复,我们没有收到您发表评论的通知。我担心在范围结束之前会调用finally块(我在其中调用DLL函数)(因为未使用变量),但是此后在SO上发现了一个问题,这消除了我的后顾之忧。我会链接到它,但不幸的是,我找不到了。
Nulano

1
disable()函数在您本来应该干净的设计上有点虚假。如果您希望仅在失败的情况下调用final,那么为什么不只使用catch语句呢?那不是为了什么吗?
user2445507 '19

32

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

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


这是非常好的一点。+1给你。不过,没有多少其他人投票赞成您。希望您不要介意我编辑了我的帖子以包含您的评论。(我当然给了你信用。)谢谢!:)
凯文(Kevin)

30

为什么即使资源被垃圾收集器自动重新分配,即使托管语言也提供了finally块?

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

就动态分配的数据而言,许多人认为您应该使用智能指针。

然而...

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

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


13
确实...从理想的角度来看,RAII看起来不错。但是我必须一直使用常规的C API(例如Win32 API中的C样式函数...)。获取返回某种HANDLE的资源是很常见的,然后需要诸如CloseHandle(HANDLE)之类的函数进行清理。最后使用try ...是处理可能的异常的一种好方法。(令人高兴的是,看起来像带有自定义删除器的shared_ptr和C ++ 11 lambdas应该提供一些基于RAII的减轻功能,不需要编写整个类来包装只在一个地方使用的某些API。)
詹姆士·约翰斯顿,

7
@JamesJohnston,编写一个包含任何类型的句柄并提供RAII机制的包装器类非常容易。例如,ATL提供了很多。您似乎认为这太麻烦了,但我不同意,它们很小并且易于编写。
Mark Ransom 2012年

5
简单的是,小的不。大小取决于您正在使用的库的复杂性。
菲利普·库林

1
@MarkRansom:如果清理期间发生异常而另一个异常未决,则RAII是否有任何机制可以使RAII进行智能化处理?在具有try / finally的系统中,可以(尽管很尴尬)安排事物,以便将挂起的异常和清理期间发生的异常都存储在new中CleanupFailedException。是否有任何可行的方法可以使用RAII实现这一结果?
2012年

3
@couling:在许多情况下,程序将调用SomeObject.DoSomething()方法并想知道它是否(1)成功,(2)没有副作用而失败,(3)失败而副作用是调用者准备应付的,或(4)因呼叫者无法应付的副作用而失败。只有呼叫者会知道它可以和不能应对的情况;呼叫者需要的是一种了解情况的方式。太糟糕了,没有提供有关异常的最重要信息的标准机制。
supercat 2012年

9

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

template <typename TCode, typename TFinallyCode>
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<const char*> &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(); });

    ...

1
对我来说,第一个是此页面中所有选项中最具可读性的。+1
Nikos '18

7

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

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

通常,您必须处理动态分配的对象,对象的动态数量等。在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”可以做的不仅仅是内存释放。


17
由于仅自动管理一种资源:内存,因此托管语言需要最终阻塞。RAII意味着可以以相同方式处理所有资源,因此最终不需要。如果您在示例中实际使用了RAII(通过在列表中使用智能指针而不是裸露的指针),则代码将比“最终”示例更简单。如果您不检查new的返回值,则更简单-检查它几乎毫无意义。
Myto 2010年

7
new不返回NULL,而是引发异常
Hasturkun 2010年

5
您提出了一个重要问题,但确实有2个可能的答案。一种是Myto给出的-将智能指针用于所有动态分配。另一种方法是使用标准容器,该容器始终在销毁时销毁其内容物。无论哪种方式,每个分配的对象最终都归静态分配的对象所有,静态分配的对象会在销毁时自动释放它。遗憾的是,由于普通指针和数组的高度可见性,程序员难以发现这些更好的解决方案。
j_random_hacker 2010年

4
C ++ 11对此进行了改进,std::shared_ptr并将其std::unique_ptr直接包含在stdlib中。
u0b34a0f6ae 2011年

16
您的示例看起来如此恐怖的原因不是因为RAII有缺陷,而是因为您没有使用它。原始指针不是RAII。
Ben Voigt

6

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调试设置将破坏此机制。


4
请记住,这不是真正的C ++异常,而是SEH异常。您可以在MS C ++代码中使用它们。SEH是OS异常处理程序,是VB,.NET实现异常的方式。
gbjbaanb

并且您可以使用SetUnhandledExceptionHandler为SEH异常创建一个“全局”未捕获的异常处理程序。
gbjbaanb

3
SEH太可怕了,还阻止了C ++析构函数的调用
paulm

6

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

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

GSL实现的示例用法如下所示:

#include <gsl/gsl_util.h>

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与您将获得的标准化功能非常接近。


3

并非如此,但是您可以对它们进行扩展,例如:

int * array = new int[10000000];
try {
  // Some code that can throw exceptions
  // ...
  throw std::exception();
  // ...
} catch (...) {
  // The finally-block (if an exception is thrown)
  delete[] array;
  // re-throw the exception.
  throw; 
}
// The finally-block (if no exception was thrown)
delete[] array;

请注意,在重新抛出原始异常之前,finally块本身可能会引发异常,从而丢弃原始异常。这与Java finally块中的行为完全相同。同样,您不能return在try&catch块中使用。


3
我很高兴您提到了finally块可能会抛出;大多数“使用RAII”答案似乎都忽略了这一点。为了避免必须两次写入finally块,您可以执行以下操作std::exception_ptr e; try { /*try block*/ } catch (...) { e = std::current_exception(); } /*finally block*/ if (e) std::rethrow_exception(e);
sethobrien

1
这就是我想知道的!为什么没有其他答案可以解释为catch(...)+空投;几乎就像一个finally块?有时您只需要它。
VinGarcia '16

我在答案(stackoverflow.com/a/38701485/566849)中提供的解决方案应允许从finally块内部引发异常。
Fabio A.

3

我想出了一个finally宏,几乎可以像 ¹Java中的finally关键字一样使用它。它使用std::exception_ptr和朋友,lambda函数和std::promise,因此需要C++11或更高;它还利用了复合语句表达式 GCC扩展,clang也支持该扩展。

警告:此答案的较早版本使用了该概念的其他实现方式,但有很多限制。

首先,让我们定义一个助手类。

#include <future>

template <typename Fun>
class FinallyHelper {
    template <typename T> struct TypeWrapper {};
    using Return = typename std::result_of<Fun()>::type;

public:    
    FinallyHelper(Fun body) {
        try {
            execute(TypeWrapper<Return>(), body);
        }
        catch(...) {
            m_promise.set_exception(std::current_exception());
        }
    }

    Return get() {
        return m_promise.get_future().get();
    }

private:
    template <typename T>
    void execute(T, Fun body) {
        m_promise.set_value(body());
    }

    void execute(TypeWrapper<void>, Fun body) {
        body();
    }

    std::promise<Return> m_promise;
};

template <typename Fun>
FinallyHelper<Fun> make_finally_helper(Fun body) {
    return FinallyHelper<Fun>(body);
}

然后是实际的宏。

#define try_with_finally for(auto __finally_helper = make_finally_helper([&] { try 
#define finally });                         \
        true;                               \
        ({return __finally_helper.get();})) \
/***/

可以这样使用:

void test() {
    try_with_finally {
        raise_exception();
    }    

    catch(const my_exception1&) {
        /*...*/
    }

    catch(const my_exception2&) {
        /*...*/
    }

    finally {
        clean_it_all_up();
    }    
}

使用std::promise使得非常容易实现,但是它可能还会引入很多不必要的开销,只需重新实现所需的功能就可以避免这些开销std::promise


¹ 警告:有没有很喜欢的Java版本工作的几件事情finally。从我的头顶上:

  1. 它不可能从外环与突破break的语句从内部trycatch()块,因为它们位于lambda函数中;
  2. 在之后至少必须有一个catch()街区try C:
  3. 如果函数的返回值不是void,但tryand catch()'s块中没有返回值,则编译将失败,因为finally宏将扩展为要返回的代码void。这可能是错了,一个虚空由具有编finally_noreturn各种各样的宏。

总而言之,我不知道自己是否会使用过这些东西,但是玩起来很有趣。:)


是的,这只是一个快速的技巧,但是如果程序员知道他们在做什么,那么它可能仍然有用。
Fabio A.

@MarkLakata,我用更好的实现更新了帖子,该实现支持抛出异常和返回。
Fabio A.

看起来挺好的。您可以通过catch(xxx) {}finally宏的开头放置一个不可能的块来摆脱第2条警告,其中xxx是伪造的类型,仅出于拥有至少一个catch块的目的。
马克·拉卡塔'16

@MarkLakata,我也想到了,但这将使其无法使用catch(...),不是吗?
Fabio A.

我不这么认为。只需xxx在私有命名空间中构成一个晦涩的类型,它将永远不会使用。
Mark Lakata '16

2

我有一个用例,我认为它finally 应该是C ++ 11语言的一个完全可以接受的部分,因为从流的角度看,它更容易阅读。我的用例是消费者/生产者线程链,其中nullptr在运行结束时发送哨兵以关闭所有线程。

如果C ++支持它,则您希望代码如下所示:

    extern Queue downstream, upstream;

    int Example()
    {
        try
        {
           while(!ExitRequested())
           {
             X* x = upstream.pop();
             if (!x) break;
             x->doSomething();
             downstream.push(x);
           } 
        }
        finally { 
            downstream.push(nullptr);
        }
    }

我认为将finally声明放在循环的开头是更合乎逻辑的,因为它是在循环退出之后发生的...但这是一厢情愿的想法,因为我们无法在C ++中完成它。请注意,该队列downstream已连接到另一个线程,因此您不能将其放入push(nullptr)析构函数中的哨兵,downstream因为此时无法将其销毁...它需要保持活动状态,直到另一个线程收到该线程为止。nullptr

所以这是如何在lambda上使用RAII类来做同样的事情:

    class Finally
    {
    public:

        Finally(std::function<void(void)> callback) : callback_(callback)
        {
        }
        ~Finally()
        {
            callback_();
        }
        std::function<void(void)> callback_;
    };

这是您的用法:

    extern Queue downstream, upstream;

    int Example()
    {
        Finally atEnd([](){ 
           downstream.push(nullptr);
        });
        while(!ExitRequested())
        {
           X* x = upstream.pop();
           if (!x) break;
           x->doSomething();
           downstream.push(x);
        }
    }

嗨,我相信我的上述回答(stackoverflow.com/a/38701485/566849)完全可以满足您的要求。
Fabio A.

1

正如许多人所说,解决方案是使用C ++ 11功能来避免finally块。功能之一是unique_ptr

这是Mephane用RAII模式写的答案。

#include <vector>
#include <memory>
#include <list>
using namespace std;

class Foo
{
 ...
};

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

    for (int i = 0; i < input.size(); ++i)
    {
      myList.push_back(unique_ptr<Foo>(new Foo(input[i])));
    }

    DoSomeStuff(myList);
}

有关在C ++标准库容器中使用unique_ptr的更多介绍,请参见此处


0

我想提供一个替代方案。

如果要始终调用finally块,只需将其放在最后一个catch块之后(可能应该是catch( ... )捕获未知的异常)

try{
   // something that might throw exception
} catch( ... ){
   // what to do with uknown exception
}

//final code to be called always,
//don't forget that it might throw some exception too
doSomeCleanUp(); 

如果您希望在发生任何异常时最终块最后要做的事,则可以使用布尔局部变量-在运行之前,将其设置为false并将true赋值放在try块的末尾,然后在catch块之后检查变量值:

bool generalAppState = false;
try{
   // something that might throw exception

   //the very end of try block:
   generalAppState = true;
} catch( ... ){
   // what to do with uknown exception
}

//final code to be called only when exception was thrown,
//don't forget that it might throw some exception too
if( !generalAppState ){
   doSomeCleanUpOfDirtyEnd();
}

//final code to be called only when no exception is thrown
//don't forget that it might throw some exception too
else{
   cleanEnd();
}

这是行不通的,因为即使代码允许异常离开代码块,finally块的整个目的仍是执行清理。考虑:`try {//可能抛出“ B”的东西}}捕获(A&a){}最后{//如果C ++拥有它……//即使抛出“ B”,也必须发生的东西。} //如果抛出“ B”,则不会执行。恕我直言,异常的要点是减少错误处理代码,因此无论何时发生抛出,catch块都会适得其反。这就是RAII可以提供帮助的原因:如果广泛应用,则异常最重要的是顶层和底层。
2016年

1
@burlyearly尽管您的观点并不圣洁,但我理解这一点,但是在C ++中却不是这样,因此您必须将其视为模拟此行为的顶层。
jave.web 2013年

下载=请评论:)
jave.web

0

我还认为,RIIA并不是异常处理和最终处理的完全有用的替代品。顺便说一句,我也认为RIIA到处都是个坏名字。我将这些类型的类称为“管理员”,并使用它们很多。95%的时间他们既不初始化也不获取资源,他们在范围内应用某些更改,或者采用已经设置的内容并确保其已被销毁。这是困扰互联网的正式名称,我甚至因为暗示我的名字可能更好而被滥用。

我只是认为要求某些临时清单的每个复杂设置都必须编写一个包含该类的类是为了避免在清理所有备份时遇到麻烦而需要编写一个类来包含该类,这是不合理的异常类型,如果在此过程中出现问题。这将导致很多临时类,否则就没有必要了。

是的,对于设计用于管理特定资源的类或设计用于处理一组相似资源的通用类来说,这是很好的。但是,即使所涉及的所有事物都具有此类包装器,清除的协调也可能不仅仅是对析构函数的反向调用。

我认为C ++拥有一个final是完全合理的。我的意思是,jeez,在过去的几十年中,已经有很多东西粘在上面,似乎奇怪的人们会突然对像最终这样的东西变得保守,这可能是非常有用的,而且可能没有其他东西那么复杂添加了(尽管这只是我的猜测。)


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

35
可爱的成语,但不完全相同。返回try块或catch不会通过您的“ finally:”代码。
爱德华KMETT

10
值得保留这个错误答案(等级为0),因为Edward Kmett提出了非常重要的区别。
Mark Lakata 2012年

12
更大的漏洞(IMO):此代码吞噬了所有异常,finally但并非如此。
Ben Voigt
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.