是否存在“最终”出现的情况? 构造在 C++ 中有用吗?

发布于 2024-07-10 12:09:31 字数 1024 浏览 9 评论 0原文

Bjarne Stroustrup 在他的 C++ 风格和技术常见问题解答中写道,强调我的:

因为 C++ 支持一种几乎总是更好的替代方案:“资源获取即初始化”技术(TC++PL3 第 14.4 节)。 基本思想是用本地对象来表示资源,以便本地对象的析构函数释放该资源。 这样,程序员就不会忘记释放资源。 例如:

class File_handle { 
      文件* p; 
  民众: 
      File_handle(const char* n, const char* a) 
          { p = fopen(n,a);   if (p==0) 抛出 Open_error(errno);   } 
      文件句柄(文件*页) 
          { p = pp;   if (p==0) 抛出 Open_error(errno);   } 

      〜File_handle() { fclose(p);   } 

      运算符 FILE*() { 返回 p;   } 

      // ... 
  }; 

  无效 f(const char* fn) 
  { 
      文件句柄 f(fn,"rw");   // 打开fn进行读写 
      // 通过f使用文件 
  } 
  

在系统中,我们需要为每个资源提供一个“资源句柄”类。 但是,我们不必为每次资源获取都添加“finally”子句。 在现实系统中,资源获取的数量远多于资源种类,因此“资源获取即初始化”技术比使用“finally”构造产生的代码更少。

请注意,Bjarne 写的是“几乎总是更好”,而不是“总是更好”。 现在我的问题是:在什么情况下 finally 构造会比在 C++ 中使用替代构造 (RAII) 更好?

Bjarne Stroustrup writes in his C++ Style and Technique FAQ, emphasis mine:

Because C++ supports an alternative that is almost always better: The "resource acquisition is initialization" technique (TC++PL3 section 14.4). The basic idea is to represent a resource by a local object, so that the local object's destructor will release the resource. That way, the programmer cannot forget to release the resource. For example:

class File_handle {
    FILE* p;
public:
    File_handle(const char* n, const char* a)
        { p = fopen(n,a); if (p==0) throw Open_error(errno); }
    File_handle(FILE* pp)
        { p = pp; if (p==0) throw Open_error(errno); }

    ~File_handle() { fclose(p); }

    operator FILE*() { return p; }

    // ...
};

void f(const char* fn)
{
    File_handle f(fn,"rw"); // open fn for reading and writing
    // use file through f
}

In a system, we need a "resource handle" class for each resource. However, we don't have to have an "finally" clause for each acquisition of a resource. In realistic systems, there are far more resource acquisitions than kinds of resources, so the "resource acquisition is initialization" technique leads to less code than use of a "finally" construct.

Note that Bjarne writes "almost always better" and not "always better". Now for my question: What situation would a finally construct be better than using the alternative construct (RAII) in C++?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

原野 2024-07-17 12:09:31

它们之间的区别在于,析构函数强调通过将清理解决方案与所使用的类型相关联来重用清理解决方案,而 try/finally 则强调一次性清理例程。 因此,当您有与使用点相关的独特的一次性清理要求时,try/finally 会更加方便,而不是与您正在使用的类型相关联的可重用清理解决方案。

我还没有尝试过这个(几个月没有下载最近的 gcc),但这应该是真的:通过在语言中添加 lambda,C++ 现在可以拥有与 finally 等效的效果,只需编写一个名为 try_finally 的函数即可。 明显的用法:

try_finally([]
{
    // attempt to do things in here, perhaps throwing...
},
[]
{
    // this always runs, even if the above block throws...
}

当然,你必须写try_finally,但只写一次,然后就可以了。 Lambda 支持新的控制结构。

类似于:

template <class TTry, class TFinally>
void try_finally(const TTry &tr, const TFinally &fi)
{
    try
    {
        tr();
    }
    catch (...)
    {
        fi();
        throw;
    }

    fi();
}

GC 的存在和对 try/finally 而不是析构函数的偏好之间根本没有任何联系。 C++/CLI 有析构函数和 GC。 它们是正交选择。 Try/finally 和析构函数是同一问题的略有不同的解决方案,都是确定性的,都是不可替代资源所需的。

C++ 函数对象强调可重用性,但使一次性匿名函数变得痛苦。 通过添加 lambda,现在可以轻松创建匿名代码块,这避免了 C++ 传统上强调通过命名类型表达的“强制可重用性”。

The difference between them is that destructors emphasise reuse of the cleanup solution by associating it with the type being used, whereas try/finally emphasises one-off cleanup routines. So try/finally is more immediately convenient when you have a unique one-off cleanup requirement associated with the point of use, rather than a reusable cleanup solution that can be associated with a type you're using.

I haven't tried this (haven't downloaded a recent gcc for months), but it should be true: with the addition of lambdas to the language, C++ can now have the effective equivalent of finally, just by writing a function called try_finally. Obvious usage:

try_finally([]
{
    // attempt to do things in here, perhaps throwing...
},
[]
{
    // this always runs, even if the above block throws...
}

Of course, you have to write try_finally, but only once and then you're good to go. Lambdas enable new control structures.

Something like:

template <class TTry, class TFinally>
void try_finally(const TTry &tr, const TFinally &fi)
{
    try
    {
        tr();
    }
    catch (...)
    {
        fi();
        throw;
    }

    fi();
}

And there is no link at all between the presence of a GC and a preference for try/finally instead of destructors. C++/CLI has destructors and GC. They're orthogonal choices. Try/finally and destructors are slightly different solutions to the same problem, both deterministic, needed for non-fungible resources.

C++ function objects emphasise reusability but make one-off anonymous functions painful. By adding lambdas, anonymous code blocks are now easy to make, and this avoids C++'s traditional emphasis on "forced reusability" expressed through named types.

摘星┃星的人 2024-07-17 12:09:31

我认为finally块会“更好”的唯一原因是它需要更少的代码来完成同样的事情。 例如,如果您有一个资源由于某种原因不使用 RAII,则您需要编写一个类来包装该资源并在析构函数中释放它,或者使用 finally 块(如果存在)。

比较:

class RAII_Wrapper
{
    Resource *resource;

public:
    RAII_Wrapper() : resource(aquire_resource()) {}

    ~RAII_Wrapper() {
        free_resource(resource);
        delete resource;
    }

    Resource *getResource() const {
        return resource;
    }
};

void Process()
{
    RAII_Resource wrapper;
    do_something(wrapper.resource);
}

与:

void Process()
{
    try {
        Resource *resource = aquire_resource();
        do_something(resource);
    }
    finally {
        free_resource(resource);
        delete resource;
    }
}

大多数人(包括我)仍然会认为第一个版本更好,因为它不会强迫您使用 try...finally 块。 您也只需要编写一次类,而不是在每个使用资源的函数中重复代码。

编辑:就像litb提到的那样,您应该使用auto_ptr而不是手动删除指针,这会简化这两种情况。

The only reason I can think of that a finally block would be "better" is when it takes less code to accomplish the same thing. For example, if you have a resource that, for some reason doesn't use RAII, you would either need to write a class to wrap the resource and free it in the destructor, or use a finally block (if it existed).

Compare:

class RAII_Wrapper
{
    Resource *resource;

public:
    RAII_Wrapper() : resource(aquire_resource()) {}

    ~RAII_Wrapper() {
        free_resource(resource);
        delete resource;
    }

    Resource *getResource() const {
        return resource;
    }
};

void Process()
{
    RAII_Resource wrapper;
    do_something(wrapper.resource);
}

versus:

void Process()
{
    try {
        Resource *resource = aquire_resource();
        do_something(resource);
    }
    finally {
        free_resource(resource);
        delete resource;
    }
}

Most people (including me) would still argue that the first version is better, because it doesn't force you to use the try...finally block. You also only need to write the class once, not duplicate the code in every function that uses the resource.

Edit: Like litb mentioned, you should use an auto_ptr instead of deleting the pointers manually, which would simplify both cases.

凹づ凸ル 2024-07-17 12:09:31

最后用C代码连接会更好。 将现有的 C 功能封装到 RAII 中可能会很痛苦。

Finally would be better when connecting with C code. It can be a pain to have to wrap existing C functionality in RAII.

宛菡 2024-07-17 12:09:31

我认为 scopeguard 在处理一次性案例方面做得很好,最终处理得很好,同时在更一般的意义上更好,因为它可以很好地处理多个流路。

I think that scope guard does a good job at handling the one-off cases that finally handles well, while being better in the more general sense because it handles more than one flow path well.

江心雾 2024-07-17 12:09:31

我发现 finally 的主要用途是处理 C 代码,正如其他人指出的那样,C 资源可能只在代码中使用一次或两次,并不真正值得包装到符合 RAII 的代码中结构。 也就是说,使用 lambda,似乎很容易通过 dtor 调用我们在函数本身中指定的函数对象来调用一些自定义逻辑。

我发现的另一个用例是奇异的杂项代码,无论我们处于正常执行路径还是异常执行路径,都应该执行这些代码,例如打印时间戳或无论如何退出函数时的某些内容。 但对我来说,这种情况非常罕见,拥有专门针对它的语言功能似乎有些矫枉过正,而且现在使用 lambda 仍然很容易做到这一点,而不必为此目的编写一个单独的类。

在大多数情况下,我现在发现它的用例非常有限,似乎并不能真正证明对语言进行如此大的改变是合理的。 不过,我的小白日梦是通过某种方式来告诉对象的 dtor 内部该对象是通过正常执行路径还是异常执行路径被销毁。

这将简化作用域防护,不再需要 commit/dismiss 调用来接受更改,而不会在作用域防护被销毁时自动回滚它们。 这个想法是允许这样的:

ScopeGuard guard(...);

// Cause external side effects.
...

// If we managed to reach this point without facing an exception,
// dismiss/commit the changes so that the guard won't undo them
// on destruction.
guard.dismiss();

简单地变成这样:

ScopeGuard guard(...);

// Cause external side effects.
...

我总是发现需要解除范围保护有点尴尬并且容易出错,因为我有时忘记解除它们只是为了让它们撤消所有更改,让我挠头不明白为什么我的操作似乎什么也没做,直到我意识到,“哎呀,我忘了关闭范围防护。”。 这是一件小事,但大多数情况下,我会发现它更加优雅,可以消除显式作用域保护解除的需要,如果他们可以在析构函数内部告诉它们是否通过正常执行路径被销毁(此时,这是可能的)副作用应该被保留)或特殊的副作用(此时副作用应该被撤销)。

这是最微不足道的事情,但却是异常安全中最难做好的事情:回滚外部副作用。 当涉及到正确销毁本地资源时,我无法从 C++ 中获得更多。 对于这个目的来说,它已经非常理想了。 但是,对于任何允许外部副作用发生的语言来说,回滚外部副作用总是很困难的,而任何能让事情变得更容易的一点点帮助都是我一直很感激的。

The main use I'd find for finally would be when dealing with C code as others pointed out where a C resource might only be used once or twice in code and not really worth wrapping into a RAII-conforming structure. That said, with lambdas, it seems easy enough to just invoke some custom logic through a dtor invoking a function object we specify in the function itself.

The other use case I'd find is for exotic miscellaneous code that should execute regardless of whether we're in a normal or exceptional execution path, like printing a timestamp or something on exiting a function no matter what. That's such a rare case though for me that it seems like overkill to have a language feature just for it, and it's still so easy to do now with lambdas without having to write a separate class just for this purpose.

For the most part I'd find very limited use cases for it now in ways that doesn't seem to really justify such a big change to the language. My little pipe dream though is some way to tell inside an object's dtor whether the object is being destroyed through a normal execution path or an exceptional one.

That would simplify scope guards to no longer require a commit/dismiss call to accept the changes without automatically rolling them back when the scope guard is destroyed. The idea is to allow this:

ScopeGuard guard(...);

// Cause external side effects.
...

// If we managed to reach this point without facing an exception,
// dismiss/commit the changes so that the guard won't undo them
// on destruction.
guard.dismiss();

To simply become this:

ScopeGuard guard(...);

// Cause external side effects.
...

I always found the need to dismiss scope guards a little bit awkward as well as error-prone, since I've sometimes forgotten to dismiss them only to have them undo all the changes, having me scratching my head for a moment as to why my operation seemed to do nothing at all until I realized, "oops, I forgot to dismiss the scope guard.". It's a minor thing but mostly I would find it so much more elegant to eliminate the need for explicit scope guard dismissal which would be possible if they could just tell, inside their destructors, whether they're being destroyed through normal execution paths (at which point the side effects should be kept) or exceptional ones (at which point the side effects should be undone).

It's the most minor thing but in the hardest area of exception-safety to get right: rolling back external side effects. I couldn't ask for more from C++ when it comes to just destroying local resources properly. It's already quite ideal for that purpose. But rolling back external side effects has always been difficult in any language that allows them to occur in the first place, and any little teeny bit of help to make that easier like this is something I'd always appreciate.

○闲身 2024-07-17 12:09:31

在六个答案后进行编辑。

这个怎么样:

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


Object *myObject = 0;

try
{
  try
  {
    myObject = new Object(); // Create an object (Might throw exception)
  }
  catch (Exception &e)
  {
    // Do something with exception (Might throw if unhandled)
  }

  throw NoException();
}
catch (Exception &e)
{
  delete myObject;

  if (e.isException()) throw e;
}

Edit after six answers.

What about this one:

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


Object *myObject = 0;

try
{
  try
  {
    myObject = new Object(); // Create an object (Might throw exception)
  }
  catch (Exception &e)
  {
    // Do something with exception (Might throw if unhandled)
  }

  throw NoException();
}
catch (Exception &e)
{
  delete myObject;

  if (e.isException()) throw e;
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文