C++11 范围退出防护,是个好主意吗?

问题描述 投票:0回答:12

我为 C++11 编写了一个小型实用程序类,我将其用作范围保护,以便更轻松地处理异常安全和类似的事情。

看起来有点像黑客。但我很惊讶我没有在其他地方看到它使用 C++11 功能。我认为 boost 对于 C++98 有类似的东西。

但这是个好主意吗?还是有我忽略的潜在问题? boost 或类似中是否已经有类似的解决方案(具有 C++11 功能)?

    namespace detail 
    {
        template<typename T>
        class scope_exit : boost::noncopyable
        {
        public:         
            explicit scope_exit(T&& exitScope) : exitScope_(std::forward<T>(exitScope)){}
            ~scope_exit(){try{exitScope_();}catch(...){}}
        private:
            T exitScope_;
        };          

        template <typename T>
        scope_exit<T> create_scope_exit(T&& exitScope)
        {
            return scope_exit<T>(std::forward<T>(exitScope));
        }
    }


#define _UTILITY_EXIT_SCOPE_LINENAME_CAT(name, line) name##line
#define _UTILITY_EXIT_SCOPE_LINENAME(name, line) _UTILITY_EXIT_SCOPE_LINENAME_CAT(name, line)
#define UTILITY_SCOPE_EXIT(f) const auto& _UTILITY_EXIT_SCOPE_LINENAME(EXIT, __LINE__) = ::detail::create_scope_exit(f)

它的用法类似。

int main () 
{
  ofstream myfile;
  myfile.open ("example.txt");
  UTILITY_SCOPE_EXIT([&]{myfile.close();}); // Make sure to close file even in case of exception
  myfile << "Writing this to a file.\n"; // Imagine this could throw
  return 0;
}
c++ c++11
12个回答
22
投票

但这是个好主意吗?

当然。一个相关主题是RAII 范式

或者有吗 我错过了潜在的问题吗?

你不处理异常。

是 已经有一个类似的解决方案(与 C++0x 功能)在 boost 或类似的?

Alexandrescu 很久以前就想出了ScopeGuard。 Boost 和

std::tr1
都有一个名为
scoped_ptr
shared_ptr
(带有自定义删除器)的东西,可以让您完成此任务。


20
投票

郑重声明,有 Boost ScopeExit


9
投票

瞄准镜守卫绝对是个好主意。我认为范围保护概念是异常安全的有力工具。如果您可以使用 C++0x 语法制作一个比 Boost 的 ScopeExit 更安全、更简洁的版本,我认为这将非常值得您花时间。

与 Alexandrescu 的 ScopeGuard 和 Boost 的 ScopeExit 类似,D 编程语言 具有针对此类事物的直接语法。 D 编程团队认为范围保护是一个足够好的想法,因此他们将其直接添加到语言中(即它没有在库中实现)。

示例。

void foo( bool fail )
{
   scope(exit)
   {
      writeln("I'm always printed");
   }

   scope(success) writeln("The function exited normally");

   scope(error)
      writeln("The function exited with an exception.");

   if( fail )
      throw new Exception("Die Die Die!");
}

基于瞄准镜的守卫并不是什么新鲜事。它的功能可以很容易地用类析构函数(RAII 等等)来复制。也可以在 C# 或 Java 中替换为

try/finally
。哎呀,甚至 pthreads 也提供了一个基本的范围保护,称为 pthread_cleanup_push

当函数中有多个

scope(*)
语句时,作用域保护变得如此强大。它的扩展性非常好,而不是
try/finally
,后者需要超人的力量来管理两个以上的任何东西。


5
投票

如果用二元运算符替换create_scope_exit,我们可以删除括号:

class at_scope_exit
{
    template<typename F>
    struct scope_exit_fn_holder : boost::noncopyable
    {
        scope_exit_fn_holder(F&& f) : f(std::forward<F>(f)) {}

        F f;
        ~scope_exit_fn_holder() { f(); }
    };

    template<typename F>
    friend scope_exit_fn_holder<F> operator==(at_scope_exit, F&& f)
    {
        return scope_exit_fn_holder<F>(std::forward<F>(f));
    }
};

用途:

auto atScopeExit = at_scope_exit() == [&]
{
    ...
};

更新:
对应宏:

#include <boost/preprocessor/cat.hpp>

#define AT_SCOPE_EXIT auto BOOST_PP_CAT(scopeExit_, __LINE__) = at_scope_exit() == [&]
#define AT_SCOPE_EXIT_EX(...) auto BOOST_PP_CAT(scopeExit_, __LINE__) = at_scope_exit() == [__VA_ARGS__]

4
投票

郑重声明,TS 3 中有

scope_exit


2
投票

目前使用此解决方案:

struct _tag_defer {
    std::function<void()> fn;
    _tag_defer() = default;
    ~_tag_defer() { fn(); }
    void operator<<(std::function<void()> f) { fn = f; }
};
// clang-format off
#define CONCAT(a, b) CONCAT_INNER(a, b)
#define CONCAT_INNER(a, b) a ## b
#define defer_name CONCAT(__defer, __LINE__)
#define defer _tag_defer defer_name; defer_name << [&]
// clang-format on

像这样使用它:

{
    defer { last_code_to_execute_on_scope_exit(); };
    ...
    defer { first_code_to_execute_on_scope_exit(); };
}

0
投票

使用

tr1::function
tr1::unique_ptr
可以大大简化实现,如下所示:

namespace detail
{
    class ScopeGuard
    {
    public:
        explicit ScopeGuard(std::function<void()> onExitScope) 
            : onExitScope_(onExitScope), dismissed_(false)
        { }

        ~ScopeGuard()
        {
            try
            {
                if(!dismissed_)
                {
                    onExitScope_();
                }
            }
            catch(...){}
        }

        void Dismiss()
        {
            dismissed_ = true;
        }
    private:
        std::function<void()> onExitScope_;
        bool dismissed_;

        // noncopyable
    private:
        ScopeGuard(ScopeGuard const&);
        ScopeGuard& operator=(ScopeGuard const&);
    };
}

inline std::unique_ptr<detail::ScopeGuard> CreateScopeGuard(std::function<void()> onExitScope)
{
    return std::unique_ptr<detail::ScopeGuard>(new detail::ScopeGuard(onExitScope));
}

0
投票

我的 0.02 美元

struct at_scope_end
{
    std::function < void () > Action;

    at_scope_end (std::function < void () > Action) :
        Action (Action)
    {
    }

    ~at_scope_end ()
    {
        Action ();
    }
};

#define AT_SCOPE_END_CAT(x,y)    x##y
#define AT_SCOPE_END_ID(index)   AT_SCOPE_END_CAT(__sg, index)
#define AT_SCOPE_END(expr)      at_scope_end AT_SCOPE_END_ID(__LINE__) ( [&] () { expr; } );

0
投票

我们可以通过将其放在定义中来省略丑陋的 [&] 内容:

#define UTILITY_SCOPE_EXIT(f) const auto& _UTILITY_EXIT_SCOPE_LINENAME(EXIT, __LINE__) = ::detail::create_scope_exit([&]f)

然后:

UTILITY_SCOPE_EXIT({myfile.close();});

使用 MSVC++ 11.0 (VS2012) 进行测试。问候。


0
投票

这是一个好主意,但是你的课程存在一些问题。

  1. 您应该禁用 new 运算符(您不希望用户以强制调用删除的方式使用它,对吗?)
  2. 您需要一个“提交”功能,以便使其成为范围守卫而不是简单的 RAII

请注意,如果您实现第 2 点,您需要为实例化的每个作用域防护指定一个有意义的名称。一般来说,这不是问题,但它可能在您的应用程序中(或根据您的口味)。

最后,这个问题可能更适合CodeReview


0
投票

使用增强:

#include <boost/preprocessor/cat.hpp>

template<class Fn>
class ScopeGuardDetails {
    const Fn m_fn;
public:
    constexpr ScopeGuardDetails(Fn &&fn) : m_fn(fn) {}
    ~ScopeGuardDetails() { m_fn(); }
};
#define ScopeGuardName BOOST_PP_CAT(BOOST_PP_CAT(__scope_guard, _), BOOST_PP_CAT(BOOST_PP_CAT(__LINE__, _), __COUNTER__))
#define defer(stmt) const auto ScopeGuardName = [](const auto _fn) { \
    return ScopeGuardDetails<decltype(_fn)> { std::move(_fn) }; \
}([&] { stmt });

用途:

if (gdiplus::GdiplusStartup(&token, &startupInput, nullptr) == Gdiplus::Ok) {
    defer({
        gdiplus::GdiplusShutdown(token);
    });
    ...
}

0
投票

我有自己的瞄准镜。我不关心展开是由于异常还是非异常展开而发生。如果我做了一些更改,我最后会通过在范围保护上调用 ::disable() 来提交它们,从而在展开时禁用它。
使用我的作用域防护,可以链接多个作用域防护,并且如果链中的最后一个作用域防护被禁用,则在销毁时将其禁用状态传播到依赖的作用域防护。因此,如果您更改多个数据结构并希望以“事务”方式执行此操作,则您有多个范围保护,它们以相反的顺序逐步恢复这些更改,如果最后一切都很好,您可以禁用最后一个范围保护链和所有依赖的范围保护也被禁用。

这是我的代码:

#pragma once
#include <utility>

template<typename Fn>
struct invoke_on_destruct;

struct iod_base
{
private:
    template<typename Fn>
    friend struct invoke_on_destruct;
    bool m_enabled;
    iod_base *m_next;
    iod_base( iod_base *next ) :
        m_enabled( true ), 
        m_next( next )
    {
    }
    void disable()
    {
        m_enabled = false;
    }
};

template<typename Fn>
struct invoke_on_destruct final : public iod_base
{
private:
    Fn m_fn;
public:
    invoke_on_destruct( Fn &&fn, iod_base *next = nullptr )
            requires requires( Fn fn ) { { fn() }; } :
        iod_base( next ),
        m_fn( std::forward<Fn>( fn ) )
    {
    }
    ~invoke_on_destruct()
    {
        bool enabled = m_enabled;
        if( m_next )
            m_next->m_enabled = enabled;
        if( enabled )
            m_fn();
    }
    using iod_base::disable;
};
© www.soinside.com 2019 - 2024. All rights reserved.