【问题标题】:C++ RAII vs. defer? [closed]C++ RAII 与延迟? [关闭]
【发布时间】:2021-06-14 22:58:13
【问题描述】:

我最近开始学习 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 部分对上述这些实现的作用。两者有什么区别,哪一种更可取?

【问题讨论】:

  • 我看不出这有多整洁。使用 RAII,我不需要告诉对象被推迟。它只是在超出范围时处理事情。除非这里没有说明有关延迟的其他内容。
  • @Alasdair - 哦,好的。与其“花费所有时间”使用析构函数编写类,不如为每个实例的每次使用编写代码。天哪,我想这样更好。你能告诉我当你想保护其中一个东西从一个范围传递到另一个的情况下它是如何工作的吗?因为如果 that 有效,那就太好了,因为您需要一直这样做!我更喜欢类/析构函数权衡自己:你不能忘记清理。
  • 我相信std::unique_ptr 和标准库容器/函数会大大减少您需要编写的类的数量。如果它没有堆分配,你完全不用担心。
  • 是的,那些完全相同的指针同时处理您用作示例的免费和封闭用例! (您可以指定Deleter)。
  • 如前所述,析构函数和 defer/finally 之间的区别在于,析构函数只需为每个类编写一次,编译器会使用该类为每个函数添加清理工作。

标签: c++ raii


【解决方案1】:

你所说的很多内容都是基于观点的,所以我将从我自己的观点开始。

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

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

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

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

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

至于#defines -- 它们只是为了确保您的变量具有唯一的名称并简化 defer 类的构造函数。

【讨论】:

  • 加上真正的代码会做一些事情,比如将实例及其所有权从一个范围传递到另一个范围 - defer 不起作用,正确的 RAII 可以。
  • 另外一个“您不必记住”的优点是您可以测试一次清理发生,然后依赖它实际发生!您不必测试每个 use....
  • 嗯...我不相信。 (1) 我不与其他开发人员一起工作,(2) 我已经期望这样做,因为我习惯 在 Go 中这样做。因此,这比为同一件事学习新标准要容易。我想问的是有没有问题。
  • 嘿,我是一名 C++ 开发人员,我使用 fopen() -- 部分是因为我很固执,但主要是因为我不喜欢 ifstream/ofstream API 的方式工作。但是当我这样做时,我会立即将 FILE * 指针传递给一个 RAII 样式的对象,该对象的析构函数将为我提供 fclose() 它。
  • @Alasdair - 你似乎对你喜欢什么和不喜欢什么有强烈的看法。这并没有错,但是当您尝试换档并转移到不同的环境(不同的计算机语言或库或其他任何东西)时,它会阻碍您。每个人都有不同的长处和短处、哲学和态度。 C++ 已经发展了 30 多年 - 更快,最近 - 并且它受益于在那段时间构建数以万计的大型(和小型,和嵌入式)系统的经验。我的建议:学习 C++,不要只使用 C++ 语法编写 Go。
【解决方案2】:

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

RAII 的专业人士:

  • 更安全、更干燥: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,所以在宏替换后,结果如下:

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

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

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

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

【讨论】:

  • 很好的解释,谢谢。一个问题:假设这用于释放指针后面的内存,defer 宏不是避免将指针本身包装在类容器中吗?在这种情况下,因为我可以直接使用指针,所以访问速度会更快?它只包装了释放函数。 (这是我最关心的:性能。)
  • 我会回答我自己的问题:请阅读编译器应该将其优化为相同的速度。如果没有优化 unique_ptr 会更慢。
  • std::unique_ptr 没有开销(std::shared_ptr 有,但允许引用计数),编译器可能很容易内联 RAII 的代码(解除对象),将释放分配到正确的位置。跨度>
  • 此外,没有理由在传统 RAII 和实施的 RAII 之间有不同的性能defer
  • 明确地说,它只是因为编译器优化而没有开销。这并不明显,而且应该很清楚,否则人们可能会认为类没有开销......他们可能会或可能不会这样做,具体取决于它们的编译方式。
猜你喜欢
  • 2013-07-25
  • 2011-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多