【问题标题】:Does ScopeGuard use really lead to better code?ScopeGuard 的使用真的会带来更好的代码吗?
【发布时间】:2010-09-08 02:21:48
【问题描述】:

我在多年前遇到了由 Andrei Alexandrescu 和 Petru Marginean 编写的this article,其中介绍并讨论了一个名为 ScopeGuard 的实用程序类,用于编写异常安全的代码。我想知道使用这些对象进行编码是否真的会产生更好的代码,或者它是否会混淆错误处理,因为也许守卫的回调会更好地呈现在 catch 块中?有没有人有在实际生产代码中使用这些的经验?

【问题讨论】:

  • C++0x/C++11 现在使用“shared_ptr”来做到这一点。
  • 我确实看到它给了你更多的力量。数据库的例子非常好。仅使用 shared_ptr 它会调用通常只关闭连接而使用 ScopedGuard 的析构函数,您实际上可以在发生异常时回滚...

标签: c++ raii scopeguard


【解决方案1】:

它肯定会改进您的代码。您暂时提出的主张,即它是晦涩难懂的,并且代码会从 catch 块中获得价值,这在 C++ 中根本不正确,因为 RAII 是一个既定的习惯用法。 C++ 中的资源处理是通过资源获取来完成的,而垃圾回收是通过隐式析构函数调用来完成的。

另一方面,显式的catch 块会使代码膨胀并引入细微的错误,因为代码流变得更加复杂并且必须显式地进行资源处理。

RAII(包括ScopeGuards)在 C++ 中并不是一项晦涩难懂的技术,而是已确立的最佳实践。

【讨论】:

  • +1 支持现代 c++ 技术。还应该注意的是,Alexandrescu 在这里展示了一个新版本的 ScopeGuard:channel9.msdn.com/Shows/Going+Deep/…,它更易于使用。值得编辑。
【解决方案2】:

是的。

如果有一个 C++ 代码我可以推荐每个 C++ 程序员花 10 分钟学习,那就是 ScopeGuard(现在是免费提供的 Loki library 的一部分)。

我决定尝试在我正在开发的小型 Win32 GUI 程序中使用(稍作修改)版本的 ScopeGuard。你可能知道 Win32 有许多不同类型的资源需要以不同的方式关闭(例如内核句柄通常用 CloseHandle() 关闭,GDI BeginPaint() 需要与 EndPaint() 配对等)我使用了 ScopeGuard使用所有这些资源,还可以使用 new 分配工作缓冲区(例如,用于与 Unicode 之间的字符集转换)。

让我吃惊的是程序短了基本上,这是双赢的:您的代码同时变得更短、更健壮。未来的代码更改不会泄露任何东西。他们就是做不到。这有多酷?

【讨论】:

    【解决方案3】:

    我经常使用它来保护内存使用情况,即从操作系统返回的需要释放的东西。例如:

    DATA_BLOB blobIn, blobOut;
    blobIn.pbData=const_cast<BYTE*>(data);
    blobIn.cbData=length;
    
    CryptUnprotectData(&blobIn, NULL, NULL, NULL, NULL, CRYPTPROTECT_UI_FORBIDDEN, &blobOut);
    Guard guardBlob=guardFn(::LocalFree, blobOut.pbData);
    // do stuff with blobOut.pbData
    

    【讨论】:

      【解决方案4】:

      是的。

      它在 C++ 中非常重要,甚至在 D 中也有特殊的语法:

      void somefunction() {
          writeln("function enter");
          // c++ has similar constructs but not in syntax level
          scope(exit) writeln("function exit");
      
          // do what ever you do, you never miss the function exit output
      }
      

      【讨论】:

        【解决方案5】:

        我没有使用过这个特定的模板,但我以前使用过类似的东西。是的,与以不同方式实现的同样健壮的代码相比,它确实会产生更清晰的代码。

        【讨论】:

          【解决方案6】:

          我认为以上答案缺少一个重要说明。正如其他人指出的那样,您可以使用ScopeGuard 来释放分配的资源,而不受故障(异常)的影响。但这可能不是您可能想要使用范围保护的唯一事情。事实上,链接文章中的示例使用ScopeGuard 用于不同的目的:交易。简而言之,如果您有多个对象(即使这些对象正确使用 RAII)需要保持某种相关的状态,这可能会很有用。如果任何这些对象的状态更改导致异常(我认为这通常意味着其状态没有更改),那么所有已应用的更改都需要回滚。这会产生它自己的一系列问题(如果回滚也失败了怎么办?)。您可以尝试推出自己的类来管理此类相关对象,但随着这些对象数量的增加,它会变得一团糟,而且您可能还是会退回到在内部使用 ScopeGuard

          【讨论】:

            【解决方案7】:

            我不得不说,不,不,它没有。这里的答案有助于证明为什么这是一个非常糟糕的想法。资源处理应该通过可重用的类来完成。他们通过使用范围保护实现的唯一一件事就是违反 DRY 对 wazoo 的要求,并在整个代码库中复制他们的资源释放代码,而不是编写一个类来处理资源,然后就这样了。

            如果范围保护有任何实际用途,资源处理不是其中之一。在这种情况下,它们大大不如普通 RAII,因为 RAII 是重复数据删除和自动的,而范围保护是手动代码复制或破坏。

            【讨论】:

            • (+1) 我完全同意。我不明白为什么投反对票的人根本没有发表评论。
            【解决方案8】:

            我的经验表明,scoped_guard 的使用远不如您可以手写的任何可重复使用的简短 RAII 类。

            在尝试scoped_guard 之前,我已经编写了 RAII 类来

            • 绘制形状后,将 GLcolor 或 GLwidth 设置回原来的值
            • 确保文件在我拥有fopened 后拥有fclosed。
            • 在执行慢速函数期间将鼠标指针更改为齿轮/沙漏后,将鼠标指针重置为其初始状态
            • 将 QListView 的 sorting 状态重置为之前的状态,一旦我暂时完成了对其 QListViewItems 的更改——我不希望列表在每次更改单个项目的文本时重新排序...

            使用简单的 RAII 类

            以下是我的代码与我手工制作的 RAII 类的样子:

            class scoped_width {
                int m_old_width;
            public:
                scoped_width(int w) {
                    m_old_width = getGLwidth();
                    setGLwidth(w);
                }
                ~scoped_width() {
                    setGLwidth(m_old_width);
                }
            };
            
            void DrawTriangle(Tria *t)
            {
                // GLwidth=1 here
            
                auto guard = scoped_width(2); // sets GLwidth=2
            
                draw_line(t->a, t->b);
                draw_line(t->b, t->c);
                draw_line(t->c, t->a);
            
                setGLwidth(5);
            
                draw_point(t->a);
                draw_point(t->b);
                draw_point(t->c);
            
            }  // scoped_width sets GLwidth back to 1 here
            

            scoped_width 的实现非常简单,而且可重复使用。 从消费者方面来看也非常简单易读。

            使用scoped_guard (C++14)

            现在,使用scoped_guard,我必须捕获介绍器 ([]) 中的现有值,以便将其传递给守卫的回调:

            void DrawTriangle(Tria *t)
            {
                // GLwidth=1 here
            
                auto guard = sg::make_scoped_guard([w=getGLwidth()](){ setGLwidth(w); }); // capture current GLwidth in order to set it back
                setGLwidth(2); // sets GLwidth=2
            
                draw_line(t->a, t->b);
                draw_line(t->b, t->c);
                draw_line(t->c, t->a);
            
                setGLwidth(5);
            
                draw_point(t->a);
                draw_point(t->b);
                draw_point(t->c);
            
            }  // scoped_guard sets GLwidth back to 1 here
            

            以上内容甚至不适用于 C++11。 更不用说尝试以这种方式将状态引入 lambda 会伤害我的眼睛。

            使用scoped_guard (C++11)

            在 C++11 中你必须这样做:

            void DrawTriangle(Tria *t)
            {
                // GLwidth=1 here
            
                int previous_width = getGLwidth();  // explicitly capture current width 
                auto guard = sg::make_scoped_guard([=](){ setGLwidth(previous_width); }); // pass it to lambda in order to set it back
                setGLwidth(2); // sets GLwidth=2
            
                draw_line(t->a, t->b);
                draw_line(t->b, t->c);
                draw_line(t->c, t->a);
            
                setGLwidth(5);
            
                draw_point(t->a);
                draw_point(t->b);
                draw_point(t->c);
            
            }  // scoped_guard sets GLwidth back to 1 here
            

            如你所见,

            • scoped_guard snoppet 要求

              • 3 行保留以前的值(状态)并将其设置为新值,并且
              • 2 个堆栈变量(previous_widthguard,同样)保持之前的状态
            • 手工制作的RAII class需要

              • 1 可读行设置新状态并保留前一个状态,并且
              • 1 个堆栈变量 (guard) 用于保存之前的状态。

            结论

            我认为这样的例子

            void some_function() {
                sg::scoped_guard([](){ cout << "this is printed last"; }
            
                cout << "this is printed first";
            }
            

            无法证明scoped_guard 的有用性。

            我希望有人能告诉我为什么我没有从scoped_guard 获得预期收益。

            我相信通过编写简短的手工类可以更好地利用 RAII,而不是使用更通用但难以使用的 scoped_guard

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2011-06-10
              • 1970-01-01
              • 2010-10-07
              • 2010-10-22
              • 1970-01-01
              • 2018-04-09
              • 2018-10-29
              • 2018-02-28
              相关资源
              最近更新 更多