4

请假设我有一个接受指针作为参数的函数。此函数可以抛出异常,因为它用于std::vector<>::push_back()管理此指针的生命周期。如果我这样声明:

void manage(T *ptr);

并这样称呼它:

manage(new T());

如果它抛出一个将指针推入的异常std::vector<>,我实际上有内存泄漏,不是吗?

会像这样声明函数:

void manage(std::auto_ptr<T> ptr);

解决我的问题?

我希望它首先std::auto_ptr在堆栈上分配 (我猜这永远不会引发异常)并让它获得对指针的所有权。安全的。

然后,在函数内部,我会将原始指针推入std::vector<>,这也是安全的:如果失败,则不会添加指针,但智能指针仍将拥有指针,因此它将被销毁。如果推送成功,我将删除智能指针对该指针的所有权并返回:这不会引发异常,所以它总是没问题的。

我的理论正确吗?

- 编辑 -

不,我想我不能那样做。这样做需要对右值进行非常量引用(从智能指针中夺走所有权)。我必须写

std::auto_ptr<T> ptr(new T());
manage(ptr);

为此,在我的情况下,这很不方便。我写这个是为了在不污染代码的情况下实现 RAII。那么,做那件事也无济于事。那将是第 22 条渔获物。

-- 编辑 2 --

将 Jason Orendorff 所说的内容拉下来供读者快速参考,最终的解决方案似乎如下:

void manage(T *ptr)
{
    std::auto_ptr<T> autoPtr(ptr);
    vector.push_back(ptr);
    autoPtr.release();
}

这解决了无用的非常量引用右值的问题。

当我完成这门课时,我正在编码,我会把它发回这里,以防有人发现它有用。

-- 编辑 3 --

好的,这里已经讨论了很多,还有一些我之前应该澄清的关键点。一般来说,当我在 stackoverflow 上发帖时,我会尝试解释我的问题背后的原因,一般来说,这是完全没用的。所以这次我想我应该直奔主题。结果发现效果不太好XD

不幸的是,我的大脑现在陷入僵局,所以我想我什至无法正确解释我最初想到的是什么来实现我的目标。我正在尝试为原子操作和异常安全的代码编写找到适合许多情况的良好解决方案,但实际上,我无法处理它 XD 我认为这是我只会随着时间的推移才能掌握的东西。

我是一个非常新的 C++ 程序员,我的重点是游戏开发。当游戏引擎中抛出异常时,就是执行的结束。系统将为我的进程释放所有内存,因此是否有一个或两个指针在这里和那里泄漏并不重要。现在我正在开发一个服务器应用程序,我发现处理异常很困难,因为异常不能使服务器崩溃;它必须“使请求崩溃”。

即“嗯,客户端,可惜开发者没有预见到这种情况,所以你以后得试试(到这里,基本上和游戏引擎一样,什么都没有修复,只是它与请求的上下文隔离,而不是整个过程。但是不要惊慌,因为一切都处于有效状态(但是,这是其中一个区别。过程没有终止,所以操作系统不能为你释放资源,而且你要注意撤消到目前为止的操作,这样你就不会完全锁定一个用户的帐户,甚至是服务器提供的全部服务)。”。

我只会越来越多地编写代码并记下我的问题,以便下次我可以写出更好的问题。我现在不准备问这个,我真的很抱歉。

非常感谢您的回复,我真的很喜欢stackoverflow。令人惊讶的是,我的问题得到了如此快速的回答,而您的回答又如此有启发性。谢谢。

4

4 回答 4

4

你可以这样做,但是如果没有抛出异常,你仍然需要清理,这似乎有点繁琐。

如果您使用boost::shared_ptr之类的东西(我相信 TR1 库中也有类似的东西 - 作为示例,请参见MS 的实现),您可能会忘记在事情按计划进行时必须进行清理。

要完成这项工作,您需要让您的向量接受boost::shared_ptr < T >实例,然后您只需清理原始实例,如果一切顺利,它将使向量中的实例保持活动状态。万一出现问题,所有boost::shared_ptr实例都将被销毁,您仍然不会泄漏。

使用智能指针,它是关于选择一个适合任务的指针,在这种情况下,共享所有权(或简单的所有权转移)似乎是一个目标,因此在大多数平台上都有比 std::auto_ptr 更好的候选者。

于 2009-11-22T03:41:59.360 回答
4

一般来说,使用std::auto_ptr作为函数参数的类型明确地告诉调用者函数将获得对象的所有权,并负责删除它。如果您的功能符合该描述,那么auto_ptr无论任何其他原因,都一定要在那里使用。

于 2009-11-22T03:58:11.687 回答
1

是的,没关系。 (见下面的编辑。)(jkp 可能会看到我错过的东西,但我认为你“在抛出异常的情况下仍然需要清理”,因为正如你所说,在这种情况下 auto_ptr将为您删除对象。)

但我认为最好还是对调用者隐藏 auto_ptr 恶作剧:

void manage(T *t) {
    std::auto_ptr<T> p(t);  // to delete t in case push_back throws
    vec.push_back(t);
    p.release();
}

编辑:我最初写的是“是的,那很好”,指的是最初的manage(auto_ptr<T>)计划,但我试过了,发现它不起作用。构造函数auto_ptr<T>::auto_ptr(T *)explicit. 编译器不允许您编写manage(new T),因为它不能将该指针隐式转换为auto_ptr. manage(T *)无论如何是一个更友好的界面!

于 2009-11-22T03:53:41.837 回答
1

我认为已经产生了足够的讨论来保证另一个答案。

首先,要回答实际问题,是的,当所有权转移发生时,通过智能指针传递参数是绝对合适的(甚至是必要的!)。通过智能指针传递是实现此目的的常见习惯用法。

void manage(std::auto_ptr<T> t) {
    ...
}
...

// The reader of this code clearly sees ownership transfer.
std::auto_ptr<T> t(new T);
manage(t);

现在,所有智能指针都有显式构造函数是有充分理由的。考虑以下函数(如果它让您喜欢,请在心理上替换为)std::auto_ptrboost::shared_ptr

void func(std::auto_ptr<Widget> w, const Gizmo& g) {
    ...
}

如果std::auto_ptr有一个隐式构造函数,突然间这段代码会编译:

func(new Widget(), gizmo);

它出什么问题了?几乎直接取自“Effective C++”,第 17 条:

func(new Widget(), a_function_that_throws());

因为在 C++ 中,参数评估的顺序是未定义的,所以您可以很好地按照以下顺序评估参数:new Widget(), a_function_that_throws(),std::auto_ptr构造函数。如果一个函数抛出,你有一个泄漏。

因此,将释放的所有资源都需要在构造时包装在 RAII 类中,然后再传递给函数。这意味着必须先构造所有智能指针,然后才能将它们作为参数传递给函数。使用 const 引用使智能指针可复制构造或隐式构造会鼓励不安全的代码。显式构造强制执行更安全的代码。

现在,你为什么不做这样的事情呢?

void manage(T *ptr)
{
    std::auto_ptr<T> autoPtr(ptr);
    vector.push_back(ptr);
    autoPtr.release();
}

如前所述,接口习惯用法告诉我,我可以传递我拥有的指针并删除它。所以,没有什么能阻止我这样做:

T item;
manage(&t);

// or
manage(&t_class_member);

这当然是灾难性的。但是您会说“我当然知道界面的含义,我永远不会那样使用它”。但是,您可能希望稍后向函数添加一个额外的参数。或者有人(不是你,或者 3 年后的你)出现了这段代码,他们可能不会像你那样看待它。

  1. 这个假设的“其他人”可能只看到没有评论的标题,并且(正确地)假设缺乏所有权转移。
  2. 他们可能会看到此函数在其他代码中的使用方式,并在不查看标头的情况下复制该用法。
  3. 他们可能会使用代码自动完成来调用函数而不阅读注释或函数,并假定缺乏所有权转移。
  4. 他们可能会编写一个包装你的函数的manage函数,但其​​他人会使用包装函数并且会错过原始函数的文档。
  5. 他们可能会尝试“扩展”您的代码,以便所有旧代码都能编译(并自动变得不安全):

    void manage(T *t, const std::string tag = some_function_that_throws());
    

如您所见,智能指针的显式构造使得在上述情况下编写不安全代码变得更加困难。

因此,我不建议违背数十年的 C++ 专业知识来制作可感知的“更好”和“有趣”的 API。

我的2c。

于 2009-11-25T11:12:20.400 回答