什么是 C++ 中的 ScopeGuard?

Posted

技术标签:

【中文标题】什么是 C++ 中的 ScopeGuard?【英文标题】:What is ScopeGuard in C++? 【发布时间】:2015-07-12 06:22:07 【问题描述】:

我的印象是它是一个包含在第三方编写的库中的 C++ 类。我尝试在 Google 上搜索,发现一篇帖子说使用它是个好主意。但是,它未能准确描述它是什么以及如何将其合并到我的代码中。谢谢。

【问题讨论】:

关于该主题的现有 SO 问题有哪些不清楚的地方?如果您阅读它们,您还会看到这可能被称为 RAII(资源获取即初始化)。例如,Does ScopeGuard use really lead to better code?。 @JamesAdkison:不,范围保护基于 RAII,就像例如for 循环基于跳转,但您不会将 for 循环称为跳转,对吗? for 循环处于更高的抽象级别,并且是一个比跳转更专业的概念。与 RAII 相比,范围保护处于更高的抽象级别,并且是一个更专业的概念。 我有一个现代的、简单的、文档化的、经过仔细测试的实现here 【参考方案1】:

ScopeGuard 曾经是范围保护 by Petru Marginean and 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 Generic: Change the Way You Write Exception-Safe Code — Forever 于 2000 年 12 月发表的原始文章仍在 Dr. Dobbs 在线 @AndrewWalker:谢谢,添加参考。 (我会再次查找并添加它,但你保存了我的工作,并提醒了我。:))。 来自 drdobbs 的原始链接已失效,但您仍然可以在 archive.org 上找到该文章:web.archive.org/web/20121009071337/http://www.drdobbs.com/cpp/…(第 1 页)、web.archive.org/web/20190317153736/http://www.drdobbs.com/cpp/…(第 2 页)、web.archive.org/web/20190317164052/http://www.drdobbs.com/cpp/…(第 3 页) ) 范围守卫是 RRID(资源释放即销毁)的一个示例,它是 RAII(资源获取即初始化)的子集。【参考方案2】:

它更像是一种设计模式,而不是特定的类。这是一种获取/释放异常安全的资源(例如文件、内存或互斥体)的方式。 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 块中并手动处理异常。

【讨论】:

到目前为止提供的两种解释中,这是更优越的。我唯一的建议是为 unique_lock 类的作用添加一些代码(这有助于使其更加明显)。除此之外,伟大而简洁的解释。 在这样的基本场景中,ulock 被标记为未使用的变量。

以上是关于什么是 C++ 中的 ScopeGuard?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Alexandrescu 不能使用 std::uncaught_exception() 在 ScopeGuard11 中实现 SCOPE_FAIL? [复制]

我在哪里可以为我的 C++ 项目找到一个好的 Scope Guard 实现?

什么是 C++ 中的“翻译单元”?

什么是 C++ 中的“查询参数”?

什么是 C++ 中的就地构造函数? [复制]

什么是 C++ 中的空指针? [复制]