C++中类似于golang的"defer"语句的实现方式

18
我正在阅读关于go语言的defer语句。它允许您指定在函数结束时要执行的操作。例如,如果您有一个文件指针或资源,而不是在每个可能的返回路径上写free/delete,您只需要一次指定defer函数即可。
看起来类似的功能最终可能会出现在C++中( C++中标准的defer/finalizer实现是什么?将会有范围保护/退出惯用语的标准化吗?)。在此之前,使用其析构函数进行回调的对象是否存在未预见的问题?看起来本地变量的析构顺序是合理的,并且它也能很好地处理异常,尽管可能无法在信号上退出。
这是一个示例实现...是否有任何问题?
#include <iostream>
#include <functional>
using namespace std;

class FrameExitTask {
    std::function<void()> func_;
public:
    FrameExitTask(std::function<void()> func) :
    func_(func) {
    }
    ~FrameExitTask() {
        func_();
    }
    FrameExitTask& operator=(const FrameExitTask&) = delete;
    FrameExitTask(const FrameExitTask&) = delete;
};

int main() {
    FrameExitTask outer_task([](){cout << "world!";});
    FrameExitTask inner_task([](){cout << "Hello, ";});
    if (1+1 == 2)
        return -1;
    FrameExitTask skipped_task([](){cout << "Blam";});
}

输出:

输出:你好,世界!


2
这可能更适合于CodeReview。请注意,已经有许多实现类似ScopeGuard的类,为什么要重新发明轮子呢?我知道一些实现具有各种好处或专业工具,我更喜欢你的版本(例如,没有类型抹消)。 - dyp
2
问题在于每个类的析构函数应该已经执行了你在做的事情,所以在C++中使用情况比在Go中要少得多。如果你正在寻找现有的实现,请查看boost.ScopeExit。Facebook的Folly也有一个,我想。 - Stephan Dollberg
1
你最好将析构函数标记为 noexcept。如果你在使用 FrameExitTask 函数时返回正常,那么你的 finally 处理程序中的异常可能会起作用。如果该函数由于其他异常而退出,则来自处理程序的第二个异常将会引起问题。 - James Henstridge
2
这与“函数式编程”标签有什么关系? - oblitum
信息:你对我在这里给出的例子是正确的,但最初激发我兴趣的应用程序并不对应于文件或其他已知类。具体而言,一个函数会进行一些检查,然后进行一些工作,当选择其中的一个时,它会将工作结果发送到其他地方。我可以实现一个单次使用的类,但想知道最好的通用方法是什么。 - daveagp
显示剩余3条评论
3个回答

30

在智能指针编程技术中,Boost讨论了这个问题:

你可以做如下操作:

#include <memory>
#include <iostream>
#include <functional>

using namespace std;
using defer = shared_ptr<void>;    

int main() {
    defer _(nullptr, bind([]{ cout << ", World!"; }));
    cout << "Hello";
}

或者,不使用 bind

#include <memory>
#include <iostream>

using namespace std;
using defer = shared_ptr<void>;    

int main() {
    defer _(nullptr, [](...){ cout << ", World!"; });
    cout << "Hello";
}

你也可以自己编写一个小类,或使用N3830 / P0052的参考实现:

C++ 核心指南有一个准则,其中使用了 gsl::finally 函数,这里有一个实现 链接

有许多代码库采用类似的解决方案,因此需要这个工具。

相关的 SO 讨论:


根据https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr的说明,删除器禁止抛出异常。 - undefined

7
这已经存在了,它被称为作用域保护。请查看这个精彩的演讲:https://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2012-Andrei-Alexandrescu-Systematic-Error-Handling-in-C。这使您可以轻松创建任意可调用项,在退出时进行调用。这是更新的版本;它最初是在go出现之前开发的。
总体而言,它的工作非常完美,但我不确定您所说的异常处理是什么意思。从必须在作用域退出时调用的函数中抛出异常会很混乱。原因是:当抛出异常(并且没有立即捕获)时,当前作用域退出。所有析构函数都将运行,并且异常将继续传播。如果其中一个析构函数抛出异常,该怎么办?现在有两个活动异常。
我想语言可能有应对这种情况的方法,但这非常复杂。在C++中,抛出异常的析构函数很少被认为是一个好主意。

我的意思是,当一个异常在函数执行过程中被抛出时,它可以正确地工作,而不是在回调函数中。了解ScopeGuard非常有帮助。谢谢! - daveagp
1
该链接已经失效。我猜想这个问题什么是C++中的ScopeGuard?提供了缺失的信息。 - undefined

-6

在C++中已经存在这个功能,但这是一个非常糟糕的想法,你提供的例子说明了为什么这是一件毫无意义的事情,我希望委员会永远不要引入它。

例如,如果你有一个文件句柄,那么写一个类来处理它,然后你就不必为每个使用情况编写延迟语句,而这些你可能会忘记或者写错。你只需要编写一个析构函数,一次就够了。然后你可以保证,在使用该类的所有情况下,它都是安全的。这样更安全,更容易。


2
在我的看法里,并没有通用规则或工具,能够解决所有问题。我是说,谈到你提出的RAII封装建议时,有些情况下你只是不想为每个特定的一次性获取/释放添加包装器来使代码库混乱。scope_guard + lambda 提供了一个很好的替代方案。 - Yuri Pozniak
1
看起来你的希望终于即将落空 :) https://github.com/jensmaurer/papers/issues/278 - oblitum

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