C++ RAII 与延迟?

C++ RAII vs. defer?

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

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

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

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

推迟实施 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 的 classes,我们使用它们。因此,必须实施 RAII 实际上意味着在可能的情况下正确选择现有标准 classes 或确保您的对象与 RAII 兼容。

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

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

所以,不,它并没有更整洁和更直观,因为您必须记住要这样做。选择正确的 classes,你不必记住。

至于 #defines -- 它们只是用来确保您的变量具有唯一的名称并缩短 defer 的构造函数 class.

It seems to me that this defer is essentially doing the same thing as RAII, but much neater and more intuitively. What is the difference, and do you see any problems with using these defer implementations instead?

RAII 高手:

  • 更安全和 DRY:RAII 避免在每次获取资源时使用 defer
  • RAII 处理转移所有权(使用移动语义)。
  • defer 可以用 RAII 实现,而不是其他方式(使用可移动资源)。
  • 使用 RAII,您可能会处理 success/error 的不同路径(例如数据库 commit/rollback 以防异常)(您可能有 finally/on_success/on_failure).
  • 可以组合(您可能有对象,有多个资源)。
  • 可以在全局范围内使用。 (即使一般应避免使用全局)。

RAII 的缺点:

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

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

对于必须 rollback/delay 更改的代码,使用 finally class 可能是合适的。应避免在 C++ 中使用 MACRO,因此我强烈建议使用 RAII 语法而不是 MACRO 方式

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

I don't really understand what the #define part does on these implementations above. What is the difference between the two and is one of them more preferable?

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

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

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

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

两者都忘记了 handle/delete copy/move 他们的类型(但是当使用宏时变量不能真正重用)。