C++中的ScopeGuard是什么?

20

我认为这是由第三方编写的库中包含的C++类。我尝试在谷歌上搜索,发现有一篇帖子说使用它是一个好主意。然而,它没有详细说明它是什么以及如何将其纳入我的代码中。谢谢。


1
现有的关于这个主题的SO问题有什么不清楚的地方吗?如果你阅读它们,你也会发现这可能被称为RAII(资源获取即初始化)。例如,Does ScopeGuard use really lead to better code? - James Adkison
@JamesAdkison:不,作用域保护 基于 RAII,就像 for 循环基于跳转一样,但你不会称 for 循环为跳转,对吧?for 循环处于更高的抽象层次,并且是比跳转更专业化的概念。作用域保护处于更高的抽象层次,并且是比 RAII 更专业化的概念。 - Cheers and hth. - Alf
我有一个现代化、简单化、文档化和经过仔细测试的实现,在这里 - ricab
2个回答

32

ScopeGuard曾经是Petru Marginean和Andrei Alexandrescu提出的一种特定的作用域保护实现链接。其思想是在作用域(即代码块)结束时,除非该作用域保护被解除,否则保护对象的析构函数将调用用户指定的清理操作。Marginean提出了一个巧妙的想法,基于对const的引用的寿命延长,为C++03声明了一个作用域保护对象。

今天,“作用域保护”更多地是一个通用的概念。

作用域保护基于RAII(自动析构函数调用用于清理),就像for循环基于跳转一样,但人们通常不会将for循环称为基于跳转的代码,因为这失去了大部分关于它是什么的信息,同样,人们通常不会将作用域保护称为RAII。 for循环处于更高级别的抽象层次,并且是比跳转更专业化的概念。作用域保护处于更高级别的抽象层次,并且是比RAII更专业化的概念。


在C++11中,作用域卫士可以通过std::function轻松实现,每个地方都可以通过lambda表达式提供清理操作。

示例:

#include <functional>       // std::function
#include <utility>          // std::move

namespace my {
    using std::function;
    using std::move;

    class Non_copyable
    {
    private:
        auto operator=( Non_copyable const& ) -> Non_copyable& = delete;
        Non_copyable( Non_copyable const& ) = delete;
    public:
        auto operator=( Non_copyable&& ) -> Non_copyable& = default;
        Non_copyable() = default;
        Non_copyable( Non_copyable&& ) = default;
    };

    class Scope_guard
        : public Non_copyable
    {
    private:
        function<void()>    cleanup_;

    public:
        friend
        void dismiss( Scope_guard& g ) { g.cleanup_ = []{}; }

        ~Scope_guard() { cleanup_(); }

        template< class Func >
        Scope_guard( Func const& cleanup )
            : cleanup_( cleanup )
        {}

        Scope_guard( Scope_guard&& other )
            : cleanup_( move( other.cleanup_ ) )
        { dismiss( other ); }
    };

}  // namespace my

#include <iostream>
void foo() {}
auto main() -> int
{
    using namespace std;
    my::Scope_guard const final_action = []{ wclog << "Finished! (Exit from main.)\n"; };

    wcout << "The answer is probably " << 6*7 << ".\n";
}

这里的function的作用是避免模板化,使得Scope_guard实例可以被声明为此类,并在各处传递。另一种选择是使用一个基于函数对象类型的类模板,略微复杂且使用受限,但可能更加高效,使用C++11中的auto进行声明,并通过工厂函数创建作用域保护实例。这两种技术都是使用简单的C++11方法来完成Marginean在C++03中使用引用生命周期扩展所做的事情。

Marginean和Alexandrescu的原始文章[通用:永远改变编写异常安全代码的方式],发表于2000年12月,仍然可以在Dr. Dobbs网站上找到。 - Andrew Walker
@AndrewWalker:谢谢,加上了那个参考文献。(我本来会再去查找并添加的,但你帮我省去了这个工作,并提醒了我。 :) ) - Cheers and hth. - Alf
1
原始的drdobbs链接已经失效,但您仍然可以在archive.org上找到这篇文章:https://web.archive.org/web/20121009071337/http://www.drdobbs.com/cpp/generic-change-the-way-you-write-excepti/184403758(第1页),https://web.archive.org/web/20190317153736/http://www.drdobbs.com/cpp/generic-change-the-way-you-write-excepti/184403758?pgno=2(第2页),https://web.archive.org/web/20190317164052/http://www.drdobbs.com/cpp/generic-change-the-way-you-write-excepti/184403758?pgno=3(第3页)。 - Claudiu
作用域守卫是 RRID(资源释放即销毁)的一个例子,它是 RAII(资源获取即初始化)的一个子集。 - Adrian McCarthy
@AdrianMcCarthy 你是从哪里了解到 RRID 这个术语的?我在简单的网络搜索中似乎找不到太多关于它的信息。 - 303
我不记得最初在哪里找到它,而且似乎搜索引擎对这样的查询越来越糟糕。许多关于RAII的文章实际上是关于RRID的。真正的RAII不仅仅是关于资源清理,而是将资源的整个生命周期与对象的生命周期绑定在一起。如果您有一个RAII对象,那么您就拥有了该资源。如果资源分配失败,则不会创建对象。因此,在关于C++异常的辩论中经常出现RAII / RRID区别。 - Adrian McCarthy

18

这更像是一种设计模式而不是一个特定的类。它是一种获取/释放资源(如文件、内存或互斥锁)的方法,可以保证异常安全。C++11中的unique_lock遵循了这种模式。

例如,使用unique_lock,我们可以不写像下面这样的代码:

void foo()
{
    myMutex.lock();
    bar();
    myMutex.unlock();
}

你编写的代码应该像这样:

void foo()
{
    unique_lock<mutex> ulock(myMutex);
    bar();
}
在第一种情况下,如果 bar 抛出异常会怎样?那么,myMutex 将永远不会被解锁,您的程序将处于无效状态。然而,在第二种情况下,unique_lock 被编程为在其构造函数中锁定互斥量,并在其析构函数中对其进行解锁。即使 bar 抛出异常,当异常向上移动时栈展开,unique_lock 将被解构,因此锁将被释放。这使您无需在每次调用 bar 时包装 try/catch 块并手动处理异常。

1
到目前为止提供的两个解释中,这个更好。我的唯一建议是添加一些unique_lock类的代码,以便更加明显。除此之外,解释很棒而且简洁明了。 - easythrees
在这样一个基本的场景中,ulock被标记为未使用的变量。 - Wouzz
1
这个答案似乎使用了 std::lock_guard 的语法,而不是 std::unique_lock - MikeOnline

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