【问题标题】:std::auto_ptr or boost::shared_ptr for pImpl idiom?std::auto_ptr 或 boost::shared_ptr 用于 pImpl 成语?
【发布时间】:2010-09-23 14:15:54
【问题描述】:

使用pImpl idiom 时,最好使用boost:shared_ptr 而不是std::auto_ptr?我确定我曾经读过 boost 版本对异常更友好?

class Foo
{
public:
    Foo();
private:
    struct impl;
    std::auto_ptr<impl> impl_;
};

class Foo
{
public:
    Foo();
private:
    struct impl;
    boost::shared_ptr<impl> impl_;
};

[编辑] 使用 std::auto_ptr 是否总是安全的,或者是否存在需要替代 boost 智能指针的情况?

【问题讨论】:

  • 有些事情让我很困扰:你愿意使用 pimpl idiom,一个编译器防火墙。所以你试图限制编译时依赖。但是,您很高兴注入智能指针实现(scoped_ptrshared_ptrauto_ptr 或其他),而您的“shell”类仅将调用转发到实现类并删除其析构函数中的指针。不知何故,我觉得如果 shell 类做更多的事情,那么设计可能会被破坏。
  • 我相信这值得对新的 C++ 进行更新。 Herb Sutter 最近在blogged 谈论这个。

标签: c++ boost stl shared-ptr auto-ptr


【解决方案1】:

您不应该为此使用 std::auto_ptr。在您声明 std::auto_ptr 时,析构函数将不可见,因此可能无法正确调用它。这是假设您正向声明您的 pImpl 类,并在另一个文件的构造函数中创建实例。

如果你使用boost::scoped_ptr(这里不需要shared_ptr,你不会与任何其他对象共享pimpl,这是由scoped_ptr强制为noncopyable),你只需要pimpl析构函数在点你调用 scoped_ptr 构造函数。

例如

// in MyClass.h

class Pimpl;

class MyClass 
{ 
private:
    std::auto_ptr<Pimpl> pimpl;

public: 
    MyClass();
};

// Body of these functions in MyClass.cpp

这里,编译器会生成MyClass的析构函数。其中必须调用 auto_ptr 的析构函数。在 auto_ptr 析构函数被实例化的地方,Pimpl 是一个不完整的类型。所以在 auto_ptr 的析构函数中,当它删除 Pimpl 对象时,它不会知道如何调用 Pimpl 析构函数。

boost::scoped_ptr(和shared_ptr)没有这个问题,因为当你调用scoped_ptr(或reset方法)的构造函数时,它也会使用一个等效的函数指针,而不是调用delete .这里的关键是当 Pimpl 不是不完整类型时,它会实例化释放函数。附带说明一下,shared_ptr 允许您指定 custom deallocation 函数,因此您可以将其用于 GDI 句柄或您可能想要的任何其他内容 - 但这对于您的需求来说太过分了。

如果您真的想使用 std::auto_ptr,那么您需要格外小心,确保在完全定义 Pimpl 时在 MyClass.cpp 中定义您的 MyClass 析构函数。

// in MyClass.h

class Pimpl;

class MyClass 
{ 
private:
    std::auto_ptr<Pimpl> pimpl;

public: 
    MyClass();
    ~MyClass();
};

// in MyClass.cpp

#include "Pimpl.h"

MyClass::MyClass() : pimpl(new Pimpl(blah))
{
}

MyClass::~MyClass() 
{
    // this needs to be here, even when empty
}

编译器将生成代码,在空析构函数中有效地析构所有 MyClass 成员。所以在 auto_ptr 析构函数被实例化时,Pimpl 不再不完整,编译器现在知道如何调用析构函数。

就我个人而言,我认为确保一切正确无误是不值得的。还有一个风险是,稍后有人会通过删除看似多余的析构函数来整理代码。所以对于这种事情,使用 boost::scoped_ptr 会更安全。

【讨论】:

  • 但是当你使用 scoped_ptr 时你不能调用 pimpl(new Pimpl(blah)) 因为它是不可复制的。那么shared_ptr不是更好吗?
  • 是否有理由在 MyClass 之外声明 Pimpl 类?我问是因为我已经养成了为 pimpl 对象使用私有结构的习惯。
  • 这个答案仍然正确吗?至少在 Boost 1.41 中,看起来 scoped_ptr 需要在类完成的地方定义析构函数和构造函数,并且直接调用 delete。
  • GCC 在 auto_ptr 用于 pimpl 的这种错误用法上给出了一个关于缺少析构函数的错误,因此无论如何都不会忘记析构函数的实现。恕我直言,这将问题减少到没有,并使 auto_ptr 优于 boost 解决方案。
  • 此答案包含错误信息。 boost::scoped_ptr 是否shared_ptr 那样在构造时使用类型擦除来捕获删除器。 std::unique_ptr 也没有。在不完整类型方面,它们都面临与std::auto_ptr 相同的问题。见scoped_ptrGOTW 100
【解决方案2】:

我倾向于使用auto_ptr。确保您的类不可复制(声明私有副本 ctor & operator=,否则继承 boost::noncopyable)。如果你使用auto_ptr,一个问题是你需要定义一个非内联析构函数,即使主体是空的。 (这是因为如果你让编译器生成默认析构函数,impl 在生成对delete impl_ 的调用时将是一个不完整的类型,调用未定义的行为)。

auto_ptr 和 boost 指针之间几乎没有选择余地。如果可以使用标准库替代方案,我倾向于不会出于文体原因使用 boost。

【讨论】:

  • 这提醒我,如果你的类确实打算是可复制的(例如,用于 STL 容器中),那么 shared_ptr 是必需的(如果你想共享 impl),或者是深拷贝的实现。
  • 如果您没有用户定义的析构函数(可能是标准部分?),您能否提供更多详细信息说明为什么它是未定义的行为?
  • 标准中没有明确说明,而是编译器在看到没有声明 dtor 时必须实例化外部类的内联默认 dtor 的结果。由于有 auto_ptr 成员变量,默认 dtor 必须调用 auto_ptr dtor。
  • 所以 ~auto_ptr 必须在此处实例化,并将包含对 delete impl_; 的调用此时在翻译单元中,impl_ 不完整,因此删除在 5.3.5 中未定义(3)。真正的编译器(例如 MSVC++)会在此处删除不完整的类型时发出警告。
  • 相反,如果你声明一个dtor,编译器不会生成一个默认的。稍后,在您的 cpp 文件中,您在顶部定义 struct impl,然后在 dtor 的(可能是空的)主体上定义。再次,~auto_ptr 被实例化,但这一次 impl 完成并定义了删除。
【解决方案3】:

std::auto_ptr 的增强选项是 boost::scoped_ptr。与auto_ptr 的主要区别在于boost::scoped_ptr 是不可复制的。

更多详情请见this page

【讨论】:

  • 这是不对的。 auto_ptr 应该是可移动的,scoped_ptr 不是。您链接的文档也确实提到了这一点:“使用 scoped_ptr 而不是 auto_ptr 的主要原因是让您的代码读者知道您打算“资源获取是初始化”仅适用于当前范围,并且无意转让所有权。”
【解决方案4】:

boost::shared_ptr 是专门为 pimpl idiom 量身定做的。主要优点之一是它允许不为持有 pimpl 的类定义析构函数。共享所有权政策可能既有优点也有缺点。但在后面的情况下,您可以正确定义复制构造函数。

【讨论】:

  • 即使它是为 pimpl 成语量身定做的,它也自带了它的本性;你共享 impl,这意味着默认情况下对象的两个副本实际上是同一个对象。这种行为令人惊讶,而您最不想做的事情就是让您班级的用户感到惊讶。
  • 您可能希望两个副本具有相同的对象。
【解决方案5】:

如果您真的很迂腐,则不能绝对保证使用auto_ptr 成员不需要在使用auto_ptr 的模板参数时对其进行完整定义。话虽如此,我从未见过这不起作用。

一种变体是使用const auto_ptr。只要您可以在初始化器列表中使用新表达式构造您的“pimpl”并保证编译器不能生成默认的复制构造函数和赋值方法,它就可以工作。仍然需要提供封闭类的非内联析构函数。

在其他条件相同的情况下,我更喜欢只使用标准库的实现,因为它使事情更便携。

【讨论】:

  • shared_ptr 和 scoped_ptr 在标准库中...... TR1。 :-)
  • 没错,但 TR1 不是 1998 或 2003 标准的一部分,并且有许多 C++ 环境可以合理地完全实现这些标准,但不提供 TR1 库。您拥有的依赖项越少;你的代码越便携。
  • shared_ptr 在 TR1 中。 scoped_ptr 不是。
【解决方案6】:

如果你想要一个可复制的类,请使用scoped_ptr,它禁止复制,因此默认情况下使你的类难以使用错误(与使用shared_ptr相比,编译器不会自行发出复制功能;并且在shared_ptr 的情况下,如果您不知道自己在做什么[即使对于巫师来说,情况也经常如此],当某个东西的副本突然也修改了该东西时,会出现奇怪的行为),然后超出定义复制构造函数和复制赋值:

class CopyableFoo {
public:
    ...
    CopyableFoo (const CopyableFoo&);
    CopyableFoo& operator= (const CopyableFoo&);
private:
    scoped_ptr<Impl> impl_;
};

...
CopyableFoo (const CopyableFoo& rhs)
    : impl_(new Impl (*rhs.impl_))
{}

【讨论】:

  • 你应该从 boost::noncopyable 派生以使你的类不可复制
  • @fmuecke:但我的CopyableFoo 旨在可复制。使用scoped_ptr&lt;&gt; 确保您不会忘记编写复制分配和构造函数。因为scoped_ptr&lt;&gt; 本身是不可复制的,所以编译器不会尝试自动发出它们。如果您使用shared_ptr&lt;&gt;,编译器将发出可能不符合您预期的复制操作。请记住,我的课程旨在是可复制的,而不是不可复制的。当然你是对的,应该使用boost::noncopyable 来做到这一点。
【解决方案7】:

对于 pImpl,shared_ptr 比 auto_ptr 更可取,因为当你复制它时,你的外部类可能会突然丢失它的指针。

使用 shared_ptr 您可以使用前向声明的类型,这样就可以了。 auto_ptr 不允许前向声明的类型。 scoped_ptr 也没有,如果你的外部类无论如何都是不可复制的并且只有一个指针,那么它也可能是一个普通的。

在 pImpl 中使用侵入式引用计数并让外部类调用其副本并在其实现中分配语义有很多话要说。假设这是一个真正的供应商(提供类)模型,供应商最好不要强迫用户使用 shared_ptr,或者使用相同版本的 shared_ptr(boost 或 std)。

【讨论】:

    【解决方案8】:

    我对@9​​87654321@ 感到非常高兴。它使创建 pImpl 变得非常容易,而无需显式地创建复制构造函数和赋值运算符。

    我已经修改了原始代码,所以它现在类似于一个 shared_ptr,所以它可以在 Epilog 代码中使用,并且保持速度。

    【讨论】:

      【解决方案9】:

      不要那么努力地打自己的脚,在 C++ 中你有很多机会 :) 没有真正需要使用自动指针,因为你完全知道你的对象应该在什么时候进入和退出生命(在你的构造函数和析构函数中)。

      保持简单。

      【讨论】:

      • 强烈不同意。这种方法不能防止 ctor 本身引起的异常,这可能在您的 impl 对象创建后发生。发生这种情况时,不会调用 dtor,并且您的 impl 对象会泄漏。
      • 如果您使用 pimpl 习惯用法,则 impl 指针是类中唯一的指针并非不可能。如果析构函数删除了指针,那么在这种情况下使用非托管指针是没有问题的。构造函数中的资源泄漏是不可能的,因为只有一个成员,只有 new 可以抛出并且不会分配内存。
      • 但问题是您现在必须在析构函数中删除一个资源,因此您还必须实现复制和分配语义,并且可能您最终还是希望进行引用计数,可能是侵入性的。
      猜你喜欢
      • 1970-01-01
      • 2012-06-11
      • 2010-11-16
      • 2010-09-16
      • 2011-09-22
      • 2011-09-27
      • 2013-06-12
      • 2013-04-16
      • 1970-01-01
      相关资源
      最近更新 更多