微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

C++ RAII 与延迟?

如何解决C++ RAII 与延迟?

我最近开始学习 C++,之前我用 Go 编程。

我最近被告知我不应该使用 new,因为抛出的异常可能会导致分配的内存不被 freed 并导致内存泄漏。一个流行的解决方案是 RAII,我找到了一个很好的解释,说明为什么要使用 RAII 以及它是什么here

然而,来自 Go 整个 RAII 的事情似乎不必要地复杂。 Go 有一个叫做 defer 的东西,它以一种非常直观的方式解决了这个问题。当范围以 defer() 结尾时,您只需包装您想要执行的操作,例如defer(free(ptr))defer(close_file(f)) 并且它会在范围的末尾自动发生。

我进行了搜索,发现了两个试图在 C++ herehere 中实现延迟功能的来源。两者最终都得到了几乎完全相同的代码,也许其中一个复制了另一个。他们在这里

推迟实施 1:

template <typename F>
struct privDefer {
    F f;
    privDefer(F f) : f(f) {}
    ~privDefer() { f(); }
};

template <typename F>
privDefer<F> defer_func(F f) {
    return privDefer<F>(f);
}

#define DEFER_1(x,y) x##y
#define DEFER_2(x,y) DEFER_1(x,y)
#define DEFER_3(x)    DEFER_2(x,__COUNTER__)
#define defer(code)   auto DEFER_3(_defer_) = defer_func([&](){code;})

推迟实施 2:

template <typename F>
struct ScopeExit {
    ScopeExit(F f) : f(f) {}
    ~ScopeExit() { f(); }
    F f;
};

template <typename F>
ScopeExit<F> MakeScopeExit(F f) {
    return ScopeExit<F>(f);
};

#define ScopE_EXIT(code) \
    auto STRING_JOIN2(scope_exit_,__LINE__) = MakeScopeExit([=](){code;})

我有两个问题:

  1. 在我看来,这个 defer 本质上与 RAII 做同样的事情,但更简洁、更直观。有什么区别?您认为使用这些 defer 实现有什么问题吗?

  2. 我不太明白 #define 部分对上述这些实现的作用。两者有什么区别,哪个更可取?

解决方法

您谈论的很多内容都是基于意见的,所以我将从我自己的意见开始。

在 C++ 世界中,我们期待 RAII。如果你想与其他开发人员相处融洽,你们都会遇到它,如果你决定以不同的方式做某事,仅仅因为这是你在 Go 中习惯的东西,你就会违背标准.

此外,C++ 开发人员不使用 FOPEN :-)。 C++ 标准库包括非常好的支持 RAII 的类,我们使用它们。因此,必须实现 RAII 实际上意味着在可能的情况下正确选择现有的标准类,或者确保您的对象与 RAII 兼容。

我几乎不需要重新设计代码来实现 RAII。我选择的类会自动处理。

因此,虽然您展示的代码很有趣,但它实际上比 RAII 的工作量更大。每次使用 FOPEN 时,您还必须记住做延迟的事情。使用 std::ifstream 或 std::ofstream 不是更容易吗?那么它已经为你处理了。 (这可以说是您的代码必须在现场实现 RAII 的其他时候。这已经通过选择正确的类来完成。)

所以,不,它不是更简洁、更直观,因为您必须记住这样做。选择正确的课程,您不必记住。

至于 #defines -- 它们只是为了确保您的变量具有唯一的名称并为 defer 类的构造函数提供快捷方式。

,

在我看来,这个 defer 本质上与 RAII 做同样的事情,但更简洁、更直观。有什么区别?您认为使用这些 defer 实现有什么问题吗?

RAII 的专家:

  • 更安全和 DRY:每次获取资源时,RAII 都避免使用 defer
  • RAII 处理转让所有权(具有移动语义)。
  • defer 可以使用 RAII 实现,而不是其他方式(使用可移动资源)。
  • 使用 RAII,您可能会处理不同的成功/错误路径(例如,发生异常时的数据库提交/回滚)(您可能有 finally/on_success/on_failure)。
  • 可以组合(您可能有对象,有多个资源)。
  • 可以在全局范围内使用。 (即使通常应该避免使用全局)。

RAII 的缺点:

  • 您需要一个“资源类型”类。 (标准提供了几种通用的,容器,智能指针,储物柜,...)。
  • 析构函数代码不应抛出。 (go 没有异常,但是 defer 的错误处理也有问题。
  • 可能会在全球范围内被滥用。 (静态订单初始化 Fiasco SIOF)。

对于真正的资源,你应该真正使用 RAII。

对于必须回滚/延迟更改的代码,使用 finally 类可能是合适的。应该避免在 C++ 中使用 MACRO,所以我强烈建议使用 RAII 语法而不是 MACRO 方式

// ..
++s[i];
const auto _ = finally([&](){ --s[i]; })
backstrack_algo(s,/*..*/);

我不太明白 #define 部分对上述这些实现的作用。两者有什么区别,哪个更可取?

两者都使用相同的技术并使用一个对象来执行 RAII。 所以宏 (#define) 是声明一个“唯一”标识符(它们的对象类型),以便能够在同一个函数中多次调用 defer,所以在 MACRO 替换后,结果类似于:

auto scope_exit_42 = MakeScopeExit([&](){ fclose(f);});

使用__COUNTER__,这不是标准的宏,但大多数编译器都支持(因此确实确保了唯一性)。 另一个使用 ___LINE__,这是标准的 MACRO,但如果您在同一行上调用 defer 两次会破坏统一性。

其他区别是默认捕获可能是 [&](作为引用,而不是按值),因为 lambda 保持在范围内,因此没有生命周期问题。

两者都忘记处理/删除它们类型的复制/移动(但因为在使用宏时变量不能真正重用)。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?